Skip to content

Desugaring Do1

do-notation is merely syntactic sugar. The compiler knows how to translate it into an expression formed using only (>>=). That's why do-notation works for any monad. The compiler simply translates it into an expression formed using (>>=), and this expression then compiles because any monad supports this operation. That's also where the magic happens. The sequence of statements inside the do-block look like they should be executed in sequence, but what this means exactly depends on the implementation of (>>=). For the Maybe monad, some statements may not be executed at all, if a statement before them in the do-block fails. That's the behaviour of (>>=) for the Maybe monad, and it is what makes Maybe look a lot like exception handling. As we will see soon, it is also possible that each statement is run many times even though it appears only once in the do-block. That's the behaviour of the list monad, which we use to model non-deterministic computations.

This may all seem like magic. I think that discussing how the compiler desugars a do-block may help to take the mystery out of how do-blocks work. In particular, it should explain how do-blocks pass the decorations of function return values along "behind the scene."

To discuss how do-blocks are desugared, we need another operator available for all monads:

(>>) :: m a -> m b -> m b
f >> g = f >>= const g

This operator captures the essence of sequencing two functions f and g where f is evaluated only for its side effect. For example, we could have written our function multiplyIfEven from before as

multiplyIfEven x y = failIfOdd x >> return (x * y)

f >> g evaluates f and binds its return value to the argument of const g. But const g ignores its argument and simply returns g. So we really just execute the two functions in sequence, and the result is whatever g returns. However, the side effects of f are preserved, because (>>=), which we use to implement (>>) combines the side effecs of f and g. We only throw away the return value of f. Our implementation of multiplyIfEven x y is not the same as multiplyIfEven2 x y = return (x * y), because multiplyIfEven x y fails if x is odd. That's the side effect of failIfOdd that is visible in the result of multiplyIfEven. Later in this chapter, we will discuss the State monad, which is probably the closest we can get to imperative code in Haskell, because it allows us to capture some state, and functions in this monad can read and modify this state. In this case, the function f in the expression f >> g may update the state, and this may both affect the state at the end of the whole computation and the behaviour of g, because g may read the state produced by f.

With (>>=) and (>>) at our disposal, here are the rules for desugaring do-blocks:

  1. A do-block consisting of a single statement,

    do <expr>
    

    is translated into

    <expr>
    

    do is not needed here, because the purpose of do is to sequence multiple statements.2

  2. A do-block of the form

    do let <variable definitions>
       <statements>
    

    gets translated into

    let <variable definitions>
    in  do <statements>
    

    The do-block after in is desugared recursively.

  3. A do-block of the form

    do <var> <- <expr>
       <statements>
    

    gets translated into

    <expr> >>= \<var> -> do <statements>
    

    The do-block in the function \<var> -> do <statements> is desugared recursively.

  4. A do-block of the form

    do <expr>
       <statements>
    

    gets translated into

    <expr> >> do <statements>
    

    The do-block after >> is desugared recursively.

According to these rules,

multiplyIfEven x y = do
    failIfOdd x
    return (x * y)

gets desugared to

multiplyIfEven x y = failIfOdd x >> do return (x * y)

using the last rule, and then to

multiplyIfEven x y = failIfOdd x >> return (x * y)

using the first rule. This is exactly the example for the use of (>>) that we discussed above.

What about this?

foo x = do let a = f x
           ( do b <- g a
                c <- h x a b
                k b c
           ) `catch` (
             handler x a
           )

This gets desugared to

foo x = let a = f x
        in  do ( do b <- g a
                    c <- h x a b
                    k b c
               ) `catch` (
                 handler x a
               )

using the second rule, then to

foo x = let a = f x
        in  ( do b <- g a
                 c <- h x a b
                 k b c
            ) `catch` (
              handler x a
            )

using the first rule, then to

foo x = let a = f x
        in  ( g a >>= \b -> do
                 c <- h x a b
                 k b c
            ) `catch` (
              handler x a
            )

using the third rule, then to

foo x = let a = f x
        in  ( g a >>= \b -> h x a b
                  >>= \c -> do k b c
            ) `catch` (
              handler x a
            )

using the third rule once more, and then to

foo x = let a = f x
        in  ( g a >>= \b -> h x a b
                  >>= \c -> k b c
            ) `catch` (
              handler x a
            )

using the first rule.

And that's exactly the ugly version of foo that we started out with at the beginning of this section. do-notation simply lets us write this in a more readable manner.

We'll use do-notation extensively in the remainder of this chapter, and in later chapters whenever we need monads, because programming using (>>=) and (>>) isn't pretty.


  1. Remember that "syntactic sugar" is a term for syntax constructs that make programming more convenient but don't add any capabilities to the language. We could have expressed the same computation using other constructs in the language. "Desugaring" is the process of translating a syntactic sugar construct into an equivalent form using other constructs. do-notation is pure syntactic sugar. It makes programming with monads much easier. However, it can be desugared to an expression that uses only (>>) and (>>=) to sequence the steps in the computation. 

  2. In particular, the first implementation of greetRepeatedly that I gave in the previous section didn't need the do. I really should have written

    greetRepeatedly :: Int -> IO ()
    greetRepeatedly reps =
        if reps == 0 then
            return ()
        else do
            putStrLn "Hello world!"
            greetRepeatedly (reps - 1)