Monads
ṱ̴̹͙̗̣̙ͮ͆͑̊̅h̸̢͔͍̘̭͍̞̹̀ͣ̅͢e̖̠ͫ̒ͦ̅̉̓̓́͟͞ ͑ͥ̌̀̉̐̂͏͚̤͜f͚͔͖̠̣͚ͤ͆ͦ͂͆̄ͥ͌o̶̡̡̝͎͎̥͖̰̭̠̊r̗̯͈̀̚b̢͙̺͚̅͝i̸̡̱̯͔̠̲̿dͧ̈ͭ̑҉͎̮d̆̓̂̏̉̏͌͆̚͝͏̺͓̜̪͓e̎ͯͨ͢҉͙̠͕͍͉n͇̼̞̙͕̮̣͈͓ͨ͐͛̽ͣ̏͆́̓ ̵ͧ̏ͤ͋̌̒͘҉̞̞̱̲͓k͔̂ͪͦ́̀͗͘n͇̰͖̓ͦ͂̇̂͌̐ȯ̸̥͔̩͒̋͂̿͌w̞̟͔̙͇̾͋̅̅̔ͅlͧ͏͎̣̲̖̥ẻ̴̢̢͎̻̹̑͂̆̽ͮ̓͋d̴̪͉̜͓̗̈ͭ̓ͥͥ͞g͊̾̋̊͊̓͑҉͏̭͇̝̰̲̤̫̥e͈̝̖̖̾ͬ̍͢͞
Monads are another level of abstraction on top of applicatives, and allow for much more flexible and expressive computation. Functors => Applicatives => Monads form a hierarchy of abstractions.
The Monad
typeclass
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
The >>=
operator is called bind, and applies a function that returns a wrapped value, to another wrapped value.
- The left operand is some monad containing a value
a
- the right operand is a function of type
a -> m b
, ie it takes somea
and returns a monad containing something of typeb
- The result is a monad of type
b
The operator can essentially be thought of as feeding the wrapped value into the function, to get a new wrapped value. x >>= f
unwraps the value in x
from it, and applies the function to f
to it. Understanding bind is key to understanding monads.
return
is just the same as pure
for applicatives, lifting the value a
into some monadic context.
Some example monad instances:
instance Monad Maybe where
Nothing >>= _ = Nothing
Just x >>= f = f x
instance Monad (Either e) where
Left l >>= _ = Left l
Right r >>= f = f r
pure = Right
instance Monad [] where
xs >>= f = concat (map f xs)
Monads give effects: composing computations sequentially using >>=
has an effect. With the State
Monad this effect is "mutation". With Maybe
and Either
the effect is that we may raise a failure at any step. Effects only happen when we want them, implemented by pure functions.
Monad Laws
For a type to be a monad, it must satisfy the following laws:
return a >>= h = h a
- Left identity
m >>= return = m
- Right identity
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
- Associativity
Example: Evaluating an Expression
A type Expr
is shown below that represents a mathematical expression, and an eval
function to evaluate it. Note that it is actually unsafe and could crash at runtime due to a div by 0 error. The safediv
function does this using Maybe
.
data Expr = Val Int | Add Expr Expr | Div Expr Expr
eval :: Expr -> Int
eval (Val n) = n
eval (Add l r) = eval l + eval r
eval (Div l r) = eval l `div` eval r
safediv :: Int -> Int -> Maybe Int
safediv x 0 = Nothing
safediv x y = Just (x `div` y)
If we want to use safediv
with eval
, we need to change it's type signature. The updated eval
is shown below using applicatives to write the function cleanly and propagate any errors:
eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Add l r) = (+) <\$> eval l <*> eval r
eval (Div l r) = safediv <\$> eval l <*> eval r
If any recursive calls return a Nothing
, the entire expression will evaluate to Nothing
. Otherwise, the <\$>
and <*>
will evaluate the expression within the Maybe
context. However, this is still wrong as the last expression now has type of Maybe (Maybe Int)
. This can be fixed using >>=
. Note the use of lambdas.
eval (Div l r) = eval l >>= \x ->
eval r >>= \y ->
x `safediv` y
The Expr
type can be extended to include a conditional expression, where If Condition True False`.
data Expr = Val Int
| Add Expr Expr
| Div Expr Expr
| If Expr Expr Expr
eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Add l r) = eval l >>= \x ->
eval r >>= \y ->
Just (x+y)
eval (Div l r) = eval l >>= \x ->
eval r >>= \y ->
x `safediv` y
eval (If c t f) = ifA <\$> eval c <*> eval t <*> eval f
where ifA b x y = if b /= 0 then x else y
With this definition using applicatives, both branches of the conditional branch are evaluated. If there is an error in the false branch, the whole expression will fail. Here, using bind, the semantics are correct.
eval' (If c t f) = eval' c >>= \b ->
if b /= 0 then eval t else eval f
<*>
vs >>=
Bind is a much more powerful abstraction than apply:
<*> :: m (a -> b) -> m a -> m b
(>>=) :: m a -> (a -> m b) -> m b
- Apply operates on functions already inside a context
- This function can't determine anything to do with the context
- With a
Maybe
, it can't determine if the overall expression returnsNothing
or not
- Bind takes a function that returns a context, and can therefore can determine more about the result of the overall expression
- It knows if it's going to return
Nothing
- It knows if it's going to return
do
Notation
Notice the pattern of >>=
being used with lambdas a fair amount. This can be tidied up with some nice syntactic sugar, called do
notation. Rewriting the earlier example:
eval :: Expr -> Maybe Int
eval (Val n) = return n
eval (Add l r) = do
x <- eval l
y <- eval r
return (x+y)
eval (Div l r) = do
x <- eval l
y <- eval r
x `safediv` y
This looks like imperative code, but is actually using monads behind the scenes. The arrows bind the results of the evaluation to some local definition, which can then be referred to further down the block.
- A block must always end with a function call that returns a monad -
- usually
return
, butsafediv
is used too
- usually
- If any of the calls within the
do
block shown returnsNothing
, the entire block will short-circuit to aNothing
.
Example: The Writer
Monad
The example of Writer
as an applicative instance can be extended to make it a Monad
instance.
data Writer w a = MkWriter (a,w)
instance Functor (Writer w) where
-- fmap :: (a -> b) -> Writer w a -> Writer w b
fmap f (MkWriter (x,o)) = MkWriter(f x, o)
instance Monoid w => Applicative (Writer w) where
-- pure :: Monoid w => a -> Writer w a
pure x = MkWriter (x, mempty)
-- <*> :: Monoid w => Writer w (a -> b) -> Writer w a -> Writer w b
MkWriter (f,o1) <*> MkWriter (x,o2) = MkWriter (f x, o1 <> o2)
instance Monoid w => Monad (Writer w) where
-- return :: Monoid w => a -> Writer w a
return = MkWriter (x, mempty) --pure
(Writer (x, o1)) >>= f = MkWriter (y, o2 <> o1)
where (MkWriter (y,o2)) = f x
Bind for Writer
applies the function to the x
value in the writer, then combines the two attached written values, and return the new value from the result of f x
along with the combined values.
Now we have a monad instance for the Writer
monad, we can rewrite our comp
function with do
notation:
comp' :: Expr -> Writer [String] Program
comp' (Val n) = do
writeLog "compiling a value"
pure [PUSH n]
comp' (Plus l r) = do writeLog "compiling a plus"
pl <- comp l
pr <- comp r
pure (pl ++ pr ++ [ADD])