First, syntax. Whitespace is application, semantically:
f x = f $ x -- "call" f with an argument x
so your expression is actually
numericBinop op params = ((mapM unpackNum) params) >>= return . Number . (foldl1 op)
Next, the operators are built from non-alphanumerical characters, without any whitespace. Here, there's .
and >>=
. Running :i (.)
and :i (>>=)
at GHCi reveals their fixity specs are infixl 9 .
and infixr 1 >>=
. 9
is above 1
so .
is stronger than >>=
; thus
= ((mapM unpackNum) params) >>= (return . Number . (foldl1 op))
infixl 9 .
means .
associates to the right, thus, finally, it is
= ((mapM unpackNum) params) >>= (return . (Number . (foldl1 op)))
The (.)
is defined as (f . g) x = f (g x)
, thus (f . (g . h)) x = f ((g . h) x) = f (g (h x)) = (f . g) (h x) = ((f . g) . h) x
; by eta-reduction we have
(f . (g . h)) = ((f . g) . h)
thus (.)
is associative, and so parenthesization is optional. We'll drop the explicit parens with the "whitespace" application from now on as well. Thus we have
numericBinop op params = (mapM unpackNum params) >>=
(\ x -> return (Number (foldl1 op x))) -- '\' is for '/\'ambda
Monadic sequences are easier written with do
, and the above is equivalent to
= do
{ x <- mapM unpackNum params -- { ; } are optional, IF all 'do'
; return (Number (foldl1 op x))) -- lines are indented at the same level
}
Next, mapM
can be defined as
mapM f [] = return []
mapM f (x:xs) = do { x <- f x ;
xs <- mapM f xs ;
return (x : xs) }
and the Monad Laws demand that
do { r <- do { x ; === do { x ;
y } ; r <- y ;
foo r foo r
} }
(you can find an overview of do
notation e.g. in this recent answer of mine); thus,
numericBinop op [a, b, ..., z] =
do {
a <- unpackNum a ;
b <- unpackNum b ;
...........
z <- unpackNum z ;
return (Number (foldl1 op [a, b, ..., z]))
}
(you may have noticed my use of x <- x
bindings -- we can use the same variable name on both sides of <-
, because monadic bindings are not recursive -- thus introducing shadowing.)
This is now clearer, hopefully.
But, I said "first, syntax". So now, the meaning of it. By same Monad Laws,
numericBinop op [a, b, ..., y, z] =
do {
xs <- do { a <- unpackNum a ;
b <- unpackNum b ;
...........
y <- unpackNum y ;
return [a, b, ..., y] } ;
z <- unpackNum z ;
return (Number (op (foldl1 op xs) z))
}
thus, we need only understand the sequencing of two "computations", c
and d
,
do { a <- c ; b <- d ; return (foo a b) }
=
c >>= (\ a ->
d >>= (\ b ->
return (foo a b) ))
for a particular monad involved, which is determined by the bind (>>=
) operator's implementation for a given monad.
Monads are EDSLs for generalized function composition. The sequencing of computations involves not only the explicit expressions appearing in the do
sequence, but also the implicit effects peculiar to the particular monad in question, performed in principled and consistent manner behind the scenes. Which is the whole point to having monads in the first place (well, one of the main points, at least).
Here the monad involved appears to concern itself with the possibility of failure, and early bail-outs in the event that failure indeed happens.
So, with the do
code we write the essence of what we intend to happen, and the possibility of intermittent failure is automatically taken care of, for us, behind the scenes.
In other words, if one of unpackNum
computations fails, so will the whole of the combined computation fail, without attempting any of the subsequent unpackNum
sub-computations. But if all of them succeed, so will the combined computation.