Monad Transformers
Prerequisites: State, Reader, and Writer
Real programs need several effects at once: configuration, state, errors, and IO. A monad transformer adds one effect on top of another monad, so you can stack them.
| Transformer | Adds | Run with |
|---|---|---|
ReaderT r m |
Read-only environment r |
runReaderT |
StateT s m |
Changing state s |
runStateT, evalStateT |
ExceptT e m |
Errors of type e |
runExceptT |
WriterT w m |
Accumulated output w |
runWriterT |
MaybeT m |
Failure without a message | runMaybeT |
State s is in fact defined as StateT s Identity — a transformer over
the "do nothing" monad.
StateT over IO
import Control.Monad.State
type App = StateT Int IO
step :: String -> App ()
step msg = do
modify (+ 1)
n <- get
liftIO (putStrLn (show n ++ ": " ++ msg))
main :: IO ()
main = do
total <- execStateT (mapM_ step ["load", "parse", "save"]) 0
putStrLn ("steps run: " ++ show total)
1: load
2: parse
3: save
steps run: 3
liftIO runs an IO action from inside any stack that has IO at the
bottom.
A three-layer stack
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Except
import Control.Monad (when)
data Config = Config {maxWithdrawal :: Int}
type Bank = ReaderT Config (StateT Int (ExceptT String IO))
withdraw :: Int -> Bank ()
withdraw amount = do
limit <- asks maxWithdrawal
balance <- get
when (amount > limit) $ throwError ("over the limit: " ++ show amount)
when (amount > balance) $ throwError "insufficient funds"
put (balance - amount)
liftIO (putStrLn ("withdrew " ++ show amount))
runBank :: Bank a -> Int -> IO (Either String (a, Int))
runBank action start = runExceptT (runStateT (runReaderT action (Config 500)) start)
main :: IO ()
main = do
runBank (withdraw 100 >> withdraw 50) 1000 >>= print
runBank (withdraw 100 >> withdraw 900) 1000 >>= print
runBank (withdraw 600) 1000 >>= print
withdrew 100
withdrew 50
Right ((),850)
withdrew 100
Left "over the limit: 900"
Left "over the limit: 600"
- The type alias
Banknames the whole stack, so function signatures stay short. runBankpeels the layers off from the outside in.- Thanks to mtl (next lesson),
asks,get, andthrowErrorwork without manually lifting through the layers.
Lifting by hand
With the plain transformers package, you call lift once for each
layer you need to pass through:
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe
import Text.Read (readMaybe)
readNumber :: String -> MaybeT IO Int
readNumber s = do
lift (putStrLn ("parsing " ++ show s))
MaybeT (return (readMaybe s))
main :: IO ()
main = do
r1 <- runMaybeT ((+) <$> readNumber "2" <*> readNumber "40")
print r1
r2 <- runMaybeT ((+) <$> readNumber "2" <*> readNumber "x")
print r2
parsing "2"
parsing "40"
Just 42
parsing "2"
parsing "x"
Nothing
Order matters
StateT s (Either e) and ExceptT e (State s) behave differently: in
the first, an error throws away the state; in the second, you get the
state back even after an error. Pick the order that matches what you
need to know after a failure.
The ReaderT pattern
Many production applications avoid deep stacks and use just ReaderT Env IO, where Env holds configuration, a logger, a database pool, and
mutable state in IORefs or TVars. Exceptions are thrown in IO. It's
simple, fast, and works well with concurrency.