do notation
| Bind with nicer syntax | |
| The three statements | |
| do in Maybe | |
| Nested do blocks |
Bind with nicer syntax
A do block is bind written vertically. Everything after <- is a monadic value, and the rest of the block becomes a function of the unwrapped result:
-- with bind
find "a" prices >>= \pa ->
find "b" prices >>= \pb ->
pure (pa + pb)
-- the same code
do pa <- find "a" prices
pb <- find "b" prices
pure (pa + pb)
Nothing changes at runtime. The notation exists because a long chain of lambdas reads inside out, and the block reads top to bottom.
The three statements
A do block has three kinds of line, and only the last one may be a plain expression:
do x <- action -- bind the result
let y = x * 2 -- pure binding, no <-
action2 -- run, ignore the result
pure y -- last line is the result
A let line inside do needs no in, because the rest of the block is the scope. A line without <- runs for its effect and discards the value.
do in Maybe
The same notation works for every monad. Here it is with Maybe, where any Nothing skips the rest:
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)
average2 a b = do
x <- safeDiv a b
y <- safeDiv x 2
pure y
If the first division fails, the block returns Nothing without evaluating the second. This short-circuit is a property of the instance, not of the syntax.
Nested do blocks
A block inside a block is fine and often clearer than one long sequence. Keep the indentation consistent or the layout rule will fight you:
report = do
a <- find "a" prices
let doubled = a * 2
b <- do
v <- find "b" prices
pure (v + doubled)
pure b
When a block grows past ten lines, split it into named functions that
return monadic values. The do stays at the top
level, and the pieces become testable on their own.
Next:
IO and purity
Article author: Arthur Isaev