Implementing Do Notation in Python
a.k.a. Evil hacks in python for fun and profit
disclaimer: This is all a really bad idea, but it was pretty fun to do. Hopefully I can teach people how to do equally horrible-yet-fun things. You should not actually use the decorator I built.
If you just want to see it in action, go to https://github.com/imh/python_do_notation.
Monads are a really useful as an abstraction, and a nice clean syntax goes a long way in making them pain-free to use.
In Haskell, that’s do
notation.
Scala has a similar for
notation.
I spend most of my time in python, though, and it doesn’t have anything analogous.
Can we make a pain-free equivalent in python?
We want to find a way to make something that looks nice act like a sequence of lambdas chained to each other with bind statements. If you squint really hard and turn your head, taking one piece of code and making it do something else is the problem that decorators solve, and as it turns out, you can make a decorator to do pretty much anything in python. I’ll start with a decorator overview for those who haven’t used them. Skip this section if you’re comfortable building these already.
Python decorators
A python decorator is a function that takes a function and returns a function. I’ll use the example of a function that logs the input and return value of another function.
This function takes a univariate function f
and returns another function that just calls f
, but does other stuff besides (printing its input and output).
We could use it to log some math like so:
We took a function double_x
and passed it to log_input_and_output
, thereby getting a new function that does all the stuff double_x
does, and then some.
A lot of times, we aren’t actually interested in the function we’re defining on its own. We only ever want the logged version. In this case we could overwrite it like so:
Now we only have the version we want. Python actually has a special way to do that to make it more convenient:
For almost all purposes, it’s equivalent to the code block above, but it looks much nicer and you can chain them together easily.
Functions of functions
So there you have it: decorators are a nice way of modifying a function you wrote and turning it into a function you didn’t. That’s just what we’re trying to do to bastardize a new monad syntax! It sounds close enough, so let’s smash this round peg into the square hole. Let’s try to build a decorator that allows something along the lines of:
With any python decorator, we get a function and return a function, but all we’re supposed to do is call it in clever ways. We can call it with different arguments, or do evil things with the output, but in the end, we still just have this black box inside. What we really need to do to add a new syntax is to break open the black box and muck around with the internals, turning it into valid python before (hopefully) packing it back up again. Round pegs don’t fit into square holes, so we need to whip out our hacksaw.
We have to stick with valid python
No matter what the decorator with_do_notation
is, if the function we’re decorating isn’t syntactically valid python and it’s just going to error.
I don’t want to write a new language that compiles to python, so I have to obey python’s syntax rules.
That means we’re searching for valid python that looks like a block of code with some semblance of associating a name to it.
It also can’t be something someone’s going to accidentally use, since we’re overriding its intended meaning.
For binding, I decided to replace haskell’s <-
with plain old equals =
.
For the block itself, with x as y:
came to mind:
Finally, there’s no type inference, so I had to hint out what the type of mreturn
should be:
It’s not the prettiest, but it doesn’t throw an exception. It’s clean enough that I’m happy.
The ast library is a lovely little hacksaw
Back to our earlier problem, we have to crack open the black box that is my_function
and rewrite the with
block inside into a sequence of binds.
For that, I need a way to inspect and modify existing python functions.
Anyone who has abused __dict__
knows that python is very friendly to this kind of thing.
It’s actually scary how friendly this language is to doing things that feel dirty.
Do it too much and you’ll start writing blog posts like this one.
For this particular use case, I found the ast
library.
It gives you tools to work with python code, traversing and modifying the AST as needed.
The official docs aren’t that great for jumping in, but there’s another set of docs called Green Tree Snakes that helped me get started.
Parsing
With the inspect
and ast
libraries, we can reconstruct the AST for our my_function
function:
That’s the AST for my_function
.
Its bulky, but you can recognize its original definition from above in it.
We can work with that.
Rewriting the AST
The goal here is to find any With
nodes and rewrite their contents.
ast
makes this super easy with ast.NodeTransformer
.
If you inherit from NodeTransformer
, you can override the visit_Foo
method to do something special to the Foo
nodes.
In this case, we want to override the With
block:
We check to make sure we’re only rewriting nodes for the With
blocks of the form with do(MyClass) as my_name:
, then rewrite the body into a sequence of monadic binds:
With these node transformers, we can finally write our decorator:
There it is! A neat decorator to implement totally new and abusive functionality in about 50 lines (plus comments). You can find the whole thing here on github.
Examples
I also wrote up some examples in that repo to see whether it works for decently and is actually painless to use.
I implemented the maybe monad, so you can write code like this:
I implemented the list monad, so you can write code like this:
And finally, for a richer test, I implemented a monadic parser. It was tremendously satisfying and was a total breeze to write with this notation. I got to write code like this:
Of course, this kind of recursive programming will exceed your max recursion depth pretty quickly, but it was great to see that, at least in principle, it’s not too hard to make it easy.