Search code examples
haskellmonadsdo-notation

Is it true that order of execution inside a do block doesn't depend on the statement order?


I was reading https://wiki.haskell.org/Do_notation_considered_harmful and was surprised to read the following lines

Newcomers might think that the order of statements determines the order of execution. ... The order of statements is also not the criterion for the evaluation order.

The wiki post gave some examples that demonstrate this property. While the examples make sense, I still don't fully believe that the statement is true, since if I write something like

main = do
  putStrLn "foo"
  putStrLn "bar"
  putStrLn "baz"

The three lines come out in the order of the statements. So what exactly is going on here?


Solution

  • What it says is that the order of statements doesn't influence the evaluation criteria. As @chi points out in IO monad effects are sequenced in order but their evaluation order is still not known. An example of a monad which will make the concept clear:

    test = do
      x <- Just (2 + undefined)
      y <- Nothing
      return (x + y)
    

    In ghci:

    λ> test
    Nothing
    

    The above code has three statements. It can be de-sugared into the following form:

    Just (2 + undefined) >>= \x -> Nothing >>= \y -> return (x + y)
    

    Now since (>>=) is left associative, it will be evaluated like this:

    (Just (2 + undefined) >>= \x -> Nothing) >>= \y -> return (x + y)

    Note that Maybe monad is defined like this:

    (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    Nothing  >>= _  =  Nothing    -- A failed computation returns Nothing
    (Just x) >>= f  =  f x        -- Applies function f to value x
    

    Applying the value (2 + undefined) to the function \x -> Nothing will result in Nothing. The expression 2 + undefined is unevaluated, thanks to lazy evaluation strategy followed by Haskell.

    Now we have a reduced form:

    Nothing >>= \y -> return (2 + undefined + y)
    

    Looking at the Monad instance for it, you can see that this will produce Nothing because Nothing >>= _ = Nothing. What if the argument was strict instead:

    test = do
      !x <- Just (2 + undefined)
      y <- Nothing
      return (y + x)
    

    Demo in ghci:

    λ> test
    *** Exception: Prelude.undefined
    

    If we follows strict evaluation procedure, then you can see that order actually matters. But in a lazy setting, the order of statements doesn't matter. And hence the wiki claims, "the order of statements is not the criterion for the evaluation order".