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 ais a computation that can read and replace a state of types, and returns ana. Underneath, it's just a functions -> (a, s).get,put,modifyread and change the state.runStatereturns both result and final state;evalStatejust the result;execStatejust 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.