Category Theory Key idea: Three everyday patterns — shared configuration, an accumulating log, threaded state — turn out to be Monad instances you can derive from scratch, the same way Parser Combinators derived a whole parsing library

Reader, Writer, State: Monads You Build By Hand

Effects Beyond IO leaned on mtl's MonadReader and MonadState without ever showing what Reader or State actually are. This chapter builds all three — Reader, Writer, and State — from nothing but function wrapping, the same from-scratch derivation Parser Combinators gave to parsing.

Three patterns, three monads

Some computations need something a plain Maybe or [] can’t express: reading a value that’s the same throughout an entire computation without threading it through every single argument by hand; accumulating a log alongside an ordinary result; or passing an evolving piece of state from one step to the next, purely. Each of these turns out to be an ordinary Monad instance, built the exact same way Parser Combinators built Parser — wrap a function, then write Functor, Applicative, and Monad for the wrapper.

Reader: shared configuration, without the threading

Passing a Config value as an extra argument to every function that might need it works, but it’s tedious the moment a call chain gets more than two or three functions deep. Reader fixes this by making “read the shared environment” part of the monad itself:

newtype Reader r a = Reader { runReader :: r -> a }

instance Functor (Reader r) where
  fmap f (Reader g) = Reader (f . g)

instance Applicative (Reader r) where
  pure a = Reader (const a)
  Reader f <*> Reader g = Reader (\r -> f r (g r))

instance Monad (Reader r) where
  Reader g >>= f = Reader (\r -> runReader (f (g r)) r)

ask :: Reader r r
ask = Reader id

ask is the one primitive that actually does anything — it hands back the environment itself, unchanged. Everything else is ordinary do-notation from there:

data Config = Config { verbosity :: Int, prefix :: String }

greet :: Reader Config String
greet = do
  cfg <- ask
  return (prefix cfg ++ "Hello!")

ghci> runReader greet (Config 1 ">> ")
">> Hello!"

greet never receives Config as an explicit argument — runReader supplies it once, at the very end, and every ask throughout the do-block sees the identical value, no matter how deeply nested the calls are.

Writer: accumulating output alongside a result

Writer is Reader’s mirror image — instead of reading a fixed value, it accumulates one, using exactly the Monoid machinery this book already justified in full:

newtype Writer w a = Writer { runWriter :: (a, w) }

instance Functor (Writer w) where
  fmap f (Writer (a, w)) = Writer (f a, w)

instance Monoid w => Applicative (Writer w) where
  pure a = Writer (a, mempty)
  Writer (f, w1) <*> Writer (a, w2) = Writer (f a, w1 <> w2)

instance Monoid w => Monad (Writer w) where
  Writer (a, w) >>= f =
    let Writer (b, w') = f a
    in Writer (b, w <> w')

tell :: w -> Writer w ()
tell w = Writer ((), w)
logStep :: Int -> Writer [String] Int
logStep x = do
  tell ["got " ++ show x]
  let y = x * 2
  tell ["doubled to " ++ show y]
  return y

ghci> runWriter (logStep 5)
(10,["got 5","doubled to 10"])

Every tell call along the way gets combined with <> — Monoids’ own combining operator — into one accumulated log, threaded invisibly through every >>=, arriving at the end as a single [String] alongside the actual result. w1 <> w2 in the Applicative instance is exactly why Writer needs a Monoid constraint at all: it’s the concrete instance of “combine two accumulated somethings” the abstract Monoid interface was built to cover.

State: threading a value through a sequence of steps

State generalizes the pattern Parser Combinators already used, without naming it — a Parser genuinely was a state-passing computation, threading “how much input is left” through every step. State makes that pattern explicit and reusable for any kind of state at all:

newtype State s a = State { runState :: s -> (a, s) }

instance Functor (State s) where
  fmap f (State g) = State (\s -> let (a, s') = g s in (f a, s'))

instance Applicative (State s) where
  pure a = State (\s -> (a, s))
  State f <*> State g = State (\s ->
    let (h, s')  = f s
        (a, s'') = g s'
    in (h a, s''))

instance Monad (State s) where
  State g >>= f = State (\s ->
    let (a, s') = g s
    in runState (f a) s')

get :: State s s
get = State (\s -> (s, s))

put :: s -> State s ()
put s = State (\_ -> ((), s))
tick :: State Int Int
tick = do
  n <- get
  put (n + 1)
  return n

ghci> runState (do { a <- tick; b <- tick; c <- tick; return [a,b,c] }) 0
([0,1,2],3)

Each tick reads the current counter, stores the incremented value, and returns the value it saw before incrementing — the classic “hand out unique IDs” pattern. Threading s from one step to the next happens entirely inside >>=; nothing in tick’s own do-block manually passes a counter anywhere, the same way nothing in a Parser’s combinators manually passed the remaining input around.

Three calls to tick threading state 0 to 1 to 2 to 3, returning values 0, 1, 2

Figure: Each tick call receives the state the previous call left behind, and passes its own updated state to the next — >>= is doing the threading, invisibly, the same way it threaded remaining input through Parser’s combinators.

⚠Common Pitfall

State’s s -> (a, s) shape looks like it should be an efficient way to simulate mutable variables, and for reasoning about a program it is — but it is still built entirely from ordinary, immutable values under the hood. Each runState call genuinely builds a new pair, not an in-place update; the “mutation” is an illusion >>= maintains by always passing the latest state forward, never a literal memory write the way Chapter 8’s ST or Practical Haskell’s IORef perform.

Where this lands: mtl and Effects Beyond IO

Effects Beyond IO introduced MonadReader and MonadState as the typeclasses that let ask, get, and put “see through” an arbitrarily deep transformer stack — but stopped short of showing what Reader and State themselves actually are. This chapter is that missing piece: the real Control.Monad.Reader, Control.Monad.Writer, and Control.Monad.State (from the mtl package) are, underneath their more polished interfaces, exactly the three hand-rolled types above, with the same Functor/Applicative/Monad instances doing the same work.

★Cool Fact

RWS — a real, standard module in mtl — fuses all three into a single monad in one step, Reader plus Writer plus State combined, for the common case of a computation that needs read-only configuration, an accumulating log, and mutable-feeling state all at once, without manually stacking three separate transformers to get there.

In the Wild

State in particular shows up constantly outside toy counter examples: parser and compiler implementations use it to thread a symbol table or a source position; game logic uses it to thread a whole game world through a sequence of turns; and any code that would reach for a mutable variable in an imperative language often reaches for State in Haskell instead, keeping the same step-by-step feel while staying provably pure the entire way through.

Three monads, three genuinely different shapes — but every one of them was, in the end, just a function wrapped in a newtype, with Functor, Applicative, and Monad instances derived from what that function needed to do. That is the whole trick this book has been demonstrating since Parser Combinators: monads are not a fixed list to memorize, but a shape worth recognizing, built by hand, exactly as needed, every time.