Skip to content
AgileLogics
7 min read

State, Reader, and Writer

Prerequisites: Monads

Pure functions can't modify variables, but they can simulate it by passing state along explicitly. The State, Reader, and Writer monads (from the mtl package, which ships with GHC) do that plumbing for you.

State: a value that changes as you go

import Control.Monad.State

counterDemo :: State Int String
counterDemo = do
  n <- get
  put (n + 1)
  modify (* 10)
  m <- get
  return ("started at " ++ show n ++ ", now " ++ show m)

labelAll :: [String] -> State Int [String]
labelAll = mapM $ \item -> do
  n <- get
  put (n + 1)
  return (show n ++ ". " ++ item)

main :: IO ()
main = do
  print (runState counterDemo 4)
  print (evalState (labelAll ["tea", "cake", "jam"]) 1)
  print (execState (mapM_ (\x -> modify (+ x)) [1 .. 10]) 0)
("started at 4, now 50",50)
["1. tea","2. cake","3. jam"]
55
  • State s a is a computation that can read and replace a state of type s, and returns an a. Underneath, it's just a function s -> (a, s).
  • get, put, modify read and change the state.
  • runState returns both result and final state; evalState just the result; execState just the state.

Reader: shared, read-only configuration

import Control.Monad.Reader

data Config = Config {appName :: String, verbose :: Bool}

greeting :: String -> Reader Config String
greeting user = do
  name <- asks appName
  v <- asks verbose
  let base = "Welcome to " ++ name ++ ", " ++ user
  return (if v then base ++ " (verbose mode)" else base)

greetAll :: [String] -> Reader Config [String]
greetAll = mapM greeting

main :: IO ()
main = do
  mapM_ putStrLn (runReader (greetAll ["Ada", "Alan"]) (Config "Notes" False))
  putStrLn (runReader (local (\c -> c {verbose = True}) (greeting "Grace")) (Config "Notes" False))
Welcome to Notes, Ada
Welcome to Notes, Alan
Welcome to Notes, Grace (verbose mode)

Reader saves you from passing a Config argument through every function. asks reads part of it; local runs a sub-computation with a modified environment.

Writer: accumulating output

import Control.Monad.Writer

collatz :: Int -> Writer [Int] Int
collatz 1 = do
  tell [1]
  return 0
collatz n = do
  tell [n]
  steps <- collatz (if even n then n `div` 2 else 3 * n + 1)
  return (steps + 1)

main :: IO ()
main = do
  let (steps, path) = runWriter (collatz 6)
  print steps
  print path
8
[6,3,10,5,16,8,4,2,1]

tell appends to a log that must be a Monoid (here a list). Writer is handy for small logs, but it can leak memory over long computations — for real logging, use a State or a proper logging library.

A note on mtl 2.3

Since mtl 2.3, Control.Monad.State and friends no longer re-export Control.Monad. If older tutorials use forM_ or when with only import Control.Monad.State, add import Control.Monad yourself.