Monads
Prerequisites: Applicative
Monads have a reputation for being mysterious, but the idea is small: a Monad lets you chain computations where each step depends on the result of the previous one, while the monad handles some extra concern — possible failure, multiple results, I/O, state — for you.
The problem
Suppose each step can fail:
import qualified Data.Map as Map
managers :: Map.Map String String
managers = Map.fromList [("ann", "bob"), ("bob", "cat"), ("cat", "dan")]
-- Who is the manager's manager's manager?
grandBossNested :: String -> Maybe String
grandBossNested e = case Map.lookup e managers of
Nothing -> Nothing
Just m1 -> case Map.lookup m1 managers of
Nothing -> Nothing
Just m2 -> Map.lookup m2 managers
main :: IO ()
main = do
print (grandBossNested "ann")
print (grandBossNested "bob")
Just "dan"
Nothing
Every step repeats "if it's Nothing, stop". That repetition is what
>>= (pronounced bind) removes.
>>= and do-notation
import qualified Data.Map as Map
managers :: Map.Map String String
managers = Map.fromList [("ann", "bob"), ("bob", "cat"), ("cat", "dan")]
boss :: String -> Maybe String
boss e = Map.lookup e managers
grandBoss :: String -> Maybe String
grandBoss e = boss e >>= boss >>= boss
grandBossDo :: String -> Maybe String
grandBossDo e = do
m1 <- boss e
m2 <- boss m1
boss m2
main :: IO ()
main = do
print (grandBoss "ann", grandBossDo "ann")
print (grandBoss "bob", grandBossDo "bob")
(Just "dan",Just "dan")
(Nothing,Nothing)
m >>= fmeans: take the value out ofm, pass it tof, which returns a new wrapped value. ForMaybe, ifmisNothing,fisn't called at all.- A
doblock is just nicer syntax for a chain of>>=.x <- mmeans "bind the result ofmtoxfor the rest of the block".
-- illustrative: the class definition
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a -- same as pure
Different monads, different "extra concern"
| Monad | What >>= handles |
|---|---|
Maybe |
Stops at the first Nothing |
Either e |
Stops at the first Left, keeping the error |
[] (list) |
Tries every possibility (non-determinism) |
IO |
Performs effects in order |
State s |
Threads a changing state through (Level 3) |
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "divide by zero"
safeDiv a b = Right (a `div` b)
calc :: Int -> Int -> Int -> Either String Int
calc a b c = do
x <- safeDiv a b
y <- safeDiv x c
return (x + y)
pairs :: [(Int, Char)]
pairs = do
n <- [1, 2]
c <- "ab"
return (n, c)
main :: IO ()
main = do
print (calc 100 5 2)
print (calc 100 0 2)
print pairs
Right 30
Left "divide by zero"
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]
The same do syntax means "stop on error" for Either and "loop over
all combinations" for lists. That's the power of the abstraction: one
notation, many behaviors, and library functions like mapM and
when that work with all of them.
Useful monadic functions
import Control.Monad (forM_, when, unless, replicateM, foldM)
main :: IO ()
main = do
forM_ [1, 2, 3] $ \i ->
putStrLn ("item " ++ show i)
when (length "abc" == 3) $ putStrLn "three letters"
unless False $ putStrLn "unless runs when the condition is False"
print (replicateM 2 "01")
print (mapM (\x -> if x > 0 then Just x else Nothing) [1, 2, 3])
print (foldM (\acc x -> if x < 0 then Left x else Right (acc + x)) 0 [1, 2, -3, 4])
item 1
item 2
item 3
three letters
unless runs when the condition is False
["00","01","10","11"]
Just [1,2,3]
Left (-3)
Common mistakes
- Thinking monads are "about I/O". IO is one monad among many;
Maybe,Either, and lists are monads with no effects at all. - Using
<-on a plain value (x <- 5) — uselet x = 5instead. - Trying to "get the value out" of
IO. You don't: you keep working insidedo, and pass plain values to pure functions.