In Haskell, do-notation is syntactic sugar for writing monadic code, expressing computations as sequences of monadic actions. Every participant monadic value in the do block must belong to the same monad.
In Haskell, do-notation is syntactic sugar for writing monadic code expressing computations as sequences of monadic actions, each action possibly calculated -- purely -- from the preceding action's computed results.
Every participant monadic value in the same do
block must belong to the same monad.
do
-notation explained, in vivid colors.
do
blocks translate into monadic code, but we can view this fact as implementational detail, at first. We can treat the do
notation axiomatically, as an embedded language.
do
blocks express sequences of monadic "actions". A simplified, standardized do
-syntax is:
do { pattern1 <- action1
; pattern2 <- action2
; pattern3 <- action3
.....................
; return result
}
Each actioni
is a Haskell expression of type M ai
for some monad M
and some result type ai
, with M
the same for all i
s. (Examples are: Maybe a
, Maybe b
, Maybe c
; [a]
, [b]
, [c]
; IO a
, IO b
, IO c
; etc.)
Thus each actioni
"produces" its own result type ai
"in" the same monad M
as all the other actions in the do
block.
The overall type of a do
block expression is M an
where an
is the type of the argument result
in the final return result
action (which thus has the type M an
). Put differently, the type of the final action in the do
block is the type of the overall do
block.
In particular (and a frequent cause of confusion while learning), if actioni
is an if
expression, each of its branches must be an expression of the same M ai
type, an expression that itself could be a do
block, if it needs to sequence several M
actions.
Each patterni
becomes match-bound to the "computed" result from the corresponding actioni
, when the whole combined "computation" described by the whole do
block actually "runs" (whatever that means specifically, for the specific M
type). If the pattern is refutable, and the pattern match fails, the do
computation chain is aborted through the invocation of M
's fail
method; otherwise, the computation chain continues to the next do
line.
Each patterni
's variable is in scope from the point of its introduction and to the end of the do
block. In particular this means that the result
expression in the final return result
action may refer to any of the patterni
's variables, as may any actionj
where j > i
.
It also means that the binding construct <-
is non-recursive: the scope to its right (and above) contains the scope to its left (and below). That in turn means that if any variable is bound more than once, the deeper (nested) binding shadows the outer binding -- any use of a variable's name refers to its most recent binding only.
Wildcards _
can be used to ignore the computed value. If this is the case, the _ <-
part can be omitted altogether.
Another special case is that by Monad laws, do { .... ; x <- action ; return x }
is equivalent to do { .... ; action }
.
The translation of a do
code into actual monadic code goes by the rule
do { pat <- act ; ... }
===
act >>= f where
f pat = do { ... }
f _ = fail "error-message"
and
do { act }
===
act
Optionally, let
can also appear in a do
block, which has a simple syntax re-write:
do { actions...
; let {...} -- no "in", NB!
; more_actions...
}
===
do { actions...
; let {...}
in
do { more_actions... }
}