Skip to content
AgileLogics
8 min read

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 >>= f means: take the value out of m, pass it to f, which returns a new wrapped value. For Maybe, if m is Nothing, f isn't called at all.
  • A do block is just nicer syntax for a chain of >>=. x <- m means "bind the result of m to x for 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) — use let x = 5 instead.
  • Trying to "get the value out" of IO. You don't: you keep working inside do, and pass plain values to pure functions.