Comprehensive Monad Notation

by Nicolas Wu

Posted on 2 December 2010

Tags: Haskell

Recently, Nils Schweinsberg wrote a post which describes his work on a new GHC extension called monad comprehensions. I stumbled across the idea of monad comprehensions a while ago when I was looking at the definition of the list monad. In this article I’ll be looking at the list monad to describe the link between list comprehensions and the do notation.

The List Monad

The Monad class defines two functions return and (>>=) as follows:

class Monad m where return :: a -> m a m a (>>=) :: m a -> (a -> m b) -> m b m a(am b)m b

The instance of a monad for a list is quite straightforward, where the return simply wraps a value to become a singleton, and bind maps a function of type a -> [b] across the elements in a list and concatenates the result:

instance Monad List where return x = [x] [x] >>= f = concatMap f xs xsf xs

I find that the relationship between list comprehensions and the do notation is made quite clear by considering the following equation, which follows on directly from the definitions above:

>>= \x -> return (f x) xs\x(f x)

Here’s an interesting property that this equation satisfies when the monad in question is a list:

concatMap (\x -> return (f x)) xs = map f xs (\x(f x)) xsf xs

We can prove this by induction. When the list is empty, the proof is trivial. If the list is not empty then we have:

concatMap (\x -> return (f x)) (x : xs) (\x(f x)) (xxs) = {- definition of concatMap -} concat ( map (\x -> return (f x)) (x : xs)) (\x(f x)) (xxs)) = {- definition of map -} concat ( return f x : map (\x -> return (f x)) xs) f x(\x(f x)) xs) = {- definition of concat -} return f x ++ concat ( map (\x -> return (f x)) xs) f x(\x(f x)) xs) = {- definition of return -} ++ concat ( map (\x -> return (f x)) xs) [f x](\x(f x)) xs) = {- [x] ++ xs = x : xs -} : concat ( map (\x -> return (f x)) xs) f x(\x(f x)) xs) = {- definition of concatMap -} : concatMap (\x -> return (f x)) xs f x(\x(f x)) xs = {- induction hypothesis -} : map f xs f xf xs = {- definition of map -} map f (x : xs) f (xxs)

With this in mind, we can view the monadic bind as a list comprehension as follows:

>>= \x -> return (f x) xs\x(f x) == {- definition of (>>=) -} concatMap (\x -> return (f x)) xs (\x(f x)) xs == {- concatMap (\x -> return f x) xs = map f xs -} map f xs f xs

It doesn’t take long to realise that map f xs is equivalent to the following list comprehension:

| x <- xs ] [ f xxs ]

Since this is a monad, it’s worth seeing how it can be written in terms of do notation:

do x <- xs xs return (f x) (f x)

The interesting point here is that the use of list comprehension notation, though restricted to lists, could easily be extended to encompass arbitrary monads.

A Little History

I was amazed when I first saw this connection, and was rather surprised that I hadn’t seen it mentioned before. A History of Haskell revealed that this notation for monads had made its way in, and back out, of the Haskell Report:

April 1997. The Haskell version 1.4 report was published (139 + 73 pages), edited by Peterson and Hammond. This was a tidy-up of the 1.3 report; the only significant change is that list comprehensions were generalised to arbitrary monads, a decision that was reversed two years later.