Monads
Monads are Haskell’s central abstraction for sequential computation. A monad is a type constructor m that admits two principal operations — pure (or return) and >>= (pronounced bind) — such that a sequence of m-typed computations may be composed, with each step’s result feeding into the next. The mechanism is general: IO, Maybe, [], Either, State, Reader, Writer, STM, and dozens of others are monads, and the monad operations admit a single notation (do-notation) that works across all of them. Understanding monads is necessary to write non-trivial Haskell; the principal hurdle for newcomers is conceptual — once the pattern clicks, monads become the standard idiom for sequencing computation.
This page covers the Monad type class, the principal monads, do-notation, monad transformers (briefly), and the conventions for using each.
The Monad type class
class Applicative m => Monad m where
return :: a -> m a -- = pure (since AMP)
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m >> k = m >>= \_ -> k
The two operations:
return(orpure) — wrap a value in the monad.>>=(bind) — given a monadic valuem aand a functiona -> m b, produce a monadic valuem b.
The intuition: >>= admits sequencing — the result of one computation flows into the next. Each step’s choice of computation depends on the previous step’s result.
>> is sequencing without using the result — useful when the first action’s value is () (or otherwise uninteresting).
The Monad laws
Every Monad instance must satisfy:
-- Left identity:
return x >>= f = f x
-- Right identity:
m >>= return = m
-- Associativity:
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
The laws ensure monads compose predictably; the compiler does not check the laws, but a non-law-abiding monad produces surprising behaviour.
do-notation
Direct use of >>= produces nested-lambda code:
greet :: IO ()
greet = getLine >>= \name -> putStrLn ("Hello, " ++ name)
readTwoNumbers :: IO Int
readTwoNumbers =
getLine >>= \s1 ->
getLine >>= \s2 ->
return (read s1 + read s2)
The do-notation is syntactic sugar for >>=:
greet :: IO ()
greet = do
name <- getLine
putStrLn ("Hello, " ++ name)
readTwoNumbers :: IO Int
readTwoNumbers = do
s1 <- getLine
s2 <- getLine
return (read s1 + read s2)
The do-block contains a sequence of statements:
<expr>— evaluate, discard the result.x <- <expr>— evaluate, bind the result tox.let x = <expr>— pure binding.
The compiler translates each form to the corresponding >>= chain. The do-notation makes monadic code read like a sequence of actions.
The block’s type is the monadic type of its last expression:
do
putStrLn "starting"
line <- getLine
return (length line)
-- :: IO Int
let in do
let admits pure bindings within a do-block:
main = do
line <- getLine
let upper = map toUpper line
len = length upper
putStrLn upper
print len
The let is the same as let … in … but without the in; the bindings are scoped to the rest of the do-block.
The Maybe monad
Maybe is the simplest substantive monad:
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
Just x >>= f = f x
The intuition: a sequence of Maybe-typed computations short-circuits on Nothing. Each step that produces Nothing propagates it through the rest:
parseAge :: String -> Maybe Int
parseAge = readMaybe
parsePerson :: String -> String -> Maybe Person
parsePerson name ageStr = do
age <- parseAge ageStr
return (Person name age)
-- Without do-notation:
parsePerson name ageStr = parseAge ageStr >>= \age -> return (Person name age)
-- Or with Applicative:
parsePerson name ageStr = Person name <$> parseAge ageStr
The Maybe monad is the conventional choice for “a sequence of operations any of which may fail with no detail”.
The Either e monad
Either e carries an error value:
instance Monad (Either e) where
return = Right
Left e >>= _ = Left e
Right x >>= f = f x
The intuition: a sequence of Either e-typed computations short-circuits on Left, propagating the error:
parseAge :: String -> Either String Int
parseAge s = case reads s of
[(n, "")] -> Right n
_ -> Left ("could not parse age: " ++ s)
validateAge :: Int -> Either String Int
validateAge n
| n < 0 = Left "age must be non-negative"
| n > 150 = Left "age must be reasonable"
| otherwise = Right n
parsePerson :: String -> String -> Either String Person
parsePerson name ageStr = do
rawAge <- parseAge ageStr
age <- validateAge rawAge
return (Person name age)
The Either monad is the conventional choice for “a sequence of operations any of which may fail with a specific error”.
The list monad
The list monad represents non-determinism:
instance Monad [] where
return x = [x]
xs >>= f = concatMap f xs -- = concat (map f xs)
The intuition: a [a]-typed computation may return any number of as; bind admits “for each result, do the next computation”:
pairs :: [(Int, Int)]
pairs = do
x <- [1, 2, 3]
y <- [10, 20]
return (x, y)
-- [(1,10), (1,20), (2,10), (2,20), (3,10), (3,20)]
pythagorean :: Int -> [(Int, Int, Int)]
pythagorean n = do
a <- [1..n]
b <- [a..n]
c <- [b..n]
if a * a + b * b == c * c
then return (a, b, c)
else []
The list monad subsumes much of what list comprehensions provide:
[(x, y) | x <- [1..3], y <- [10, 20]]
-- equivalent to the do-notation pairs above
The list comprehension is conventionally clearer for simple cases; the do-form admits more elaborate logic.
The IO monad
IO is the principal monad for effectful computation. The full treatment is in IO.
The conventional IO patterns:
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name)
IO is also a monad in the formal sense — the >>= and return operations satisfy the monad laws. The mechanism that makes IO distinctive is the type system: a value of type IO a cannot be examined except in another IO context, so the type system tracks effectful code separately from pure code.
The State monad
The State monad threads state through a sequence of operations without explicit parameters:
import Control.Monad.State
newtype State s a = State { runState :: s -> (a, s) }
-- The monad operations:
get :: State s s
put :: s -> State s ()
modify :: (s -> s) -> State s ()
gets :: (s -> a) -> State s a
runState :: State s a -> s -> (a, s)
evalState :: State s a -> s -> a -- discard the final state
execState :: State s a -> s -> s -- discard the final result
A State-monad computation:
counter :: State Int Int
counter = do
n <- get
put (n + 1)
return n
main = do
let (results, finalState) = runState (replicateM 5 counter) 0
print results -- [0, 1, 2, 3, 4]
print finalState -- 5
The mechanism is the conventional Haskell substitute for mutable variables in pure code; the threading of the state is handled by the monad.
The Reader monad
Reader admits accessing a shared environment without explicit parameters:
import Control.Monad.Reader
newtype Reader r a = Reader { runReader :: r -> a }
ask :: Reader r r
asks :: (r -> a) -> Reader r a
local :: (r -> r) -> Reader r a -> Reader r a
runReader :: Reader r a -> r -> a
A Reader-monad computation:
data Config = Config { port :: Int, host :: String }
makeUrl :: Reader Config String
makeUrl = do
config <- ask
return ("http://" ++ host config ++ ":" ++ show (port config))
main = do
let config = Config { port = 8080, host = "localhost" }
putStrLn (runReader makeUrl config)
The mechanism admits dependency injection without explicit parameters.
The Writer monad
Writer admits accumulating an output value:
import Control.Monad.Writer
newtype Writer w a = Writer { runWriter :: (a, w) }
tell :: Monoid w => w -> Writer w ()
runWriter :: Writer w a -> (a, w)
execWriter :: Writer w a -> w -- discard the result
A Writer-monad computation:
fizzbuzz :: Int -> Writer [String] ()
fizzbuzz n = do
when (n `mod` 3 == 0) (tell ["fizz"])
when (n `mod` 5 == 0) (tell ["buzz"])
main = do
let (_, log) = runWriter (mapM_ fizzbuzz [1..15])
mapM_ putStrLn log
The mechanism admits accumulating logs, traces, or any other value with a Monoid instance. The Writer is generally considered the least useful of the standard monads — it is space-leak-prone and there are usually better alternatives.
The STM monad
STM (Software Transactional Memory) admits atomic concurrent updates:
import Control.Concurrent.STM
newTVar :: a -> STM (TVar a)
readTVar :: TVar a -> STM a
writeTVar :: TVar a -> a -> STM ()
atomically :: STM a -> IO a
The full treatment is in Concurrency. The principal point: STM is a monad in its own right, with its own operations that compose; the atomically function bridges from STM to IO.
Monad transformers
The single-monad approach is sometimes insufficient; programs may need both State and IO, or both Reader and Either. Monad transformers combine monads into a single richer monad:
import Control.Monad.State
import Control.Monad.Trans
-- StateT s IO a is "IO with State"
counterIO :: StateT Int IO ()
counterIO = do
n <- get
liftIO (putStrLn ("Counter: " ++ show n))
put (n + 1)
main = do
finalState <- execStateT (replicateM_ 5 counterIO) 0
print finalState
The principal transformers:
| Transformer | Effect |
|---|---|
MaybeT m | Adds Maybe (failure) to m |
ExceptT e m | Adds Either e (errors) to m |
StateT s m | Adds state to m |
ReaderT r m | Adds environment to m |
WriterT w m | Adds accumulator to m |
IdentityT m | No effect; only useful with transformer stacks |
The mtl package provides the conventional set. The MonadIO constraint admits running IO actions in any transformer stack containing IO:
class Monad m => MonadIO m where
liftIO :: IO a -> m a
liftIO admits using putStrLn and other IO actions in a StateT or ReaderT context.
The full treatment of monad transformers is a substantial topic; the conventional Haskell idiom in modern libraries is the mtl-style class-based approach (MonadState, MonadReader, MonadIO) rather than the explicit transformer-stack form.
MonadFail
class Monad m => MonadFail m where
fail :: String -> m a
MonadFail is the class for monads that admit pattern-match failure. IO’s fail throws an exception; Maybe’s produces Nothing; []’s produces [].
The class admits using non-trivial patterns in do-notation:
processFirst :: [Int] -> Maybe Int
processFirst xs = do
Just n <- listToMaybe xs -- pattern match; calls fail on Nothing
return n
If the pattern fails, the monad’s fail is invoked. The mechanism is occasionally surprising; the conventional advice is to use explicit case for non-trivial patterns in do.
Common patterns
Sequencing with >>=
import Network.HTTP.Simple
fetchAndParse :: String -> IO (Maybe Json)
fetchAndParse url = do
response <- httpJSON (parseRequest_ url)
return (Just (getResponseBody response))
The pattern is direct sequencing: each step’s result feeds the next.
Error short-circuiting with Either
processOrder :: Input -> Either String Result
processOrder input = do
raw <- parseInput input
valid <- validateOrder raw
enriched <- enrichOrderData valid
return (computeResult enriched)
The Either String chain produces “the first error” or the final result.
Stateful computation with State
runProcess :: [Item] -> State Stats ()
runProcess items = do
forM_ items $ \item -> do
stats <- get
put (updateStats item stats)
The mechanism admits explicit state without mutable variables.
Combining multiple monads with mtl-style classes
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.IO.Class
processItem :: (MonadState Stats m, MonadReader Config m, MonadIO m) => Item -> m ()
processItem item = do
config <- ask
when (verbose config) $ do
liftIO (putStrLn ("Processing " ++ itemName item))
modify (\s -> updateStats item s)
The mtl-style constraints admit polymorphic monadic code that works in any stack containing the required effects.
Applicative when the steps are independent
-- Use Applicative when the next step doesn't depend on the previous:
combine :: Maybe Int -> Maybe Int -> Maybe Int
combine = liftA2 (+)
-- or:
combine x y = (+) <$> x <*> y
-- Use Monad when there's a dependency:
divide :: Maybe Int -> Maybe Int -> Maybe Int
divide x y = do
n <- x
d <- y
if d == 0 then Nothing else Just (n `div` d)
The conventional advice: use Applicative if possible; reach for Monad when the next step depends on the previous.
A note on the conventional Haskell discipline
The full Functor/Applicative/Monad hierarchy is the foundation of Haskell’s effect typing:
Functor— map over a structure.Applicative— combine independent effectful values with a pure function.Monad— sequence effectful computations where each depends on the previous.
Most Haskell code does not write monad transformers explicitly; the mtl package provides class-based abstractions that admit polymorphic effectful code without the syntactic overhead. Modern Haskell libraries increasingly use effect systems — polysemy, freer-simple, effectful, cleff — that admit even more flexible composition of effects than the mtl-style monad transformers.
The principal advice for new Haskell code:
- Use
do-notation for monadic code; reach for the underlying>>=only when it is clearer. - Use
Maybefor “may fail with no detail”;Either efor “may fail with detail”. - Use
IOonly when actually performing I/O; pure code should remain in pure types. - Use
Stateormtl-style class abstractions for stateful computations. - Reach for monad transformers when the program needs multiple effects simultaneously.
- For complex effect compositions, consider an effect system rather than hand-stacking transformers.