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:
-
A
do
-block consisting of a single statement,do <expr>
is translated into
<expr>
do
is not needed here, because the purpose ofdo
is to sequence multiple statements.2 -
A
do
-block of the formdo let <variable definitions> <statements>
gets translated into
let <variable definitions> in do <statements>
The
do
-block afterin
is desugared recursively. -
A
do
-block of the formdo <var> <- <expr> <statements>
gets translated into
<expr> >>= \<var> -> do <statements>
The
do
-block in the function\<var> -> do <statements>
is desugared recursively. -
A
do
-block of the formdo <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.
-
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. ↩ -
In particular, the first implementation of
greetRepeatedly
that I gave in the previous section didn't need thedo
. I really should have written↩greetRepeatedly :: Int -> IO () greetRepeatedly reps = if reps == 0 then return () else do putStrLn "Hello world!" greetRepeatedly (reps - 1)