In this post, we explore the relationship between Monads and list comprehension and implement some ideas from Haskell in Python.
List comprehension is a very useful and commonly used feature in both Python and Haskell. Many people will be familiar with the following way to calculate the squares of a list of numbers in Python
This is almost exactly the same in Haskell
squares xs = [x ^ 2 | x <- xs]
But what is actually going on here? Although Python does not have static typing let's agree that the function
squares takes a list of numbers as input and returns a list of numbers as well. In Haskell, we'd write this type signature as
But why are lists interesting? A list is a type that wraps another type. We could say "list" is a context that we can put values in.1 With this notion of "contexts", we can rephrase our understanding of list comprehension. When we write
x ** 2 for x in xs what we are saying is: "
xs is a context that contains
x. Please take the value(s) out of their context and give us their square(s) in another list."
In Haskell, there is another way to express the above list comprehension using "do-notation".
squares xs = do x <- xs return (x ^ 2)
This reads almost exactly like the above formulation of our intent. Do-notation is a very generic concept though. It is syntactic sugar for dealing with generic "contexts". In Haskell they are called "monads" and have a formal definition. Put simply, for a type to be a monad there must be a
return function2 that puts a value in a context. For lists this is simply the list constructor. In Python, we could write
Additionally, there must be a function called
bind (in Haskell this is written as the infix operator
>>=) which takes a value in a context and a function from the type of that value to another type wrapped in the same context. Or formally
For lists in Python we could implement this as follows
= += return
Note that this is subtly different from mapping
xs. Now how does this relate to list comprehension and the do-notation? With those definitions of
list_bind, we could rewrite our squares function like this
Or equivalently in Haskell
squares' xs = xs >>= \x -> return (x ^ 2)
If we compare this to the version in do-notation, we can see how it's really just syntactic sugar.
squares xs = do x <- xs return (x ^ 2)
If all we ever dealt with was lists, this whole exercise would be pretty useless.3 Luckily for us lists aren't the only monads in existence. Haskell has a type called
Maybe. Instances of
Maybe can be either
Just a value or
Nothing. The monad operations for
Maybe are defined as follows
return x = Just x Nothing >>= f = Nothing Just x >>= f = Just (f x)
Earlier, we discussed how do-notation was just syntactic sugar for using
bind while for lists, list-comprehension was just syntactic sugar for do-notation. And in fact, in Haskell we can write the following (seemingly crazy) code once we activate the monad comprehensions language extension.
= [x ^ 2 | x <- mx] squareMaybe mx
which if applied on
Just 5 gives
Just 25. If it's applied on
Nothing, the result is still
As you might have noticed, our
squareMaybe function is exactly the same as our initial
squares function. With monad comprehensions activated, its type signature becomes
In Python, we could mimic this behaviour by treating
None as nothing and a value as
Just a value.
return return None return
This is pretty useful if we have a bunch of computations that could potentially return
None and we want to chain them together. While this implementation is certainly universal (it gives us the monad operations for
Maybe without a corresponding type) it doesn't give us monad comprehension.
In order to get this behaviour in Python, we need to go a little bit crazy and (ab)use iterators. What we will do is create dedicated types for
Just and make them iterable so we can use them in generator expressions. Finally we will introduce a convenience method to get around the limitation of Python's
 being closely associated with lists and nothing else.
First, let's start with
By defining the
__iter__ method, we make instances of
Nothing iterable but every iteration will immediately stop because of
next raising the
StopIteration exception when it's called.
Similarly, we can define
Just as follows.
= return yield return
Using this, we can implement
return return return
And we're almost done. In order to get comprehension for our
Maybe, we need one extra step. Right now, we could write something like
and it would work. However, the result would not be
Just 25 but rather
 which is not what we want. We would have replaced our maybe by a list. We get around this by introducing a convenience function
which allows us to write
and get the expected result
Just 25. Pretty neat, isn't it?
Feedback, questions, comments?
If you want to point out errors in the above text, have inquiries, or simply want to tell me how reading it was a complete waste of your time, feel free to go to the corresponding issue on GitHub, in order to discuss this article.
In Python, the types of a list's elements can be heterogeneous which brings with it a bunch of problems that we simply choose to ignore at this point.
Not to be confused with
return in Python. Luckily, in the future, this will be renamed to
pure. The technical details can be found on the corresponding mailing list
You might argue that it is useless anyway…
To make things a bit nicer (and not use
type), we could of course define a
bind member function in
Just and call that one from