Monad Transformers and mtl

26.1 Stacking Effects

import Control.Monad.State
import Control.Monad.Except
import Control.Monad.Reader

-- ReaderT Config (StateT Int (ExceptT String IO)) a
data Config = Config {maxAttempts :: Int}

type App = ReaderT Config (StateT Int (ExceptT String IO))

attempt :: String -> App ()
attempt guess = do
  limit <- asks maxAttempts
  n <- get
  when (n >= limit) $ throwError "too many attempts"
  put (n + 1)
  liftIO (putStrLn ("Attempt " ++ show (n + 1) ++ ": " ++ guess))   -- lift IO into the stack

runApp :: App a -> IO (Either String (a, Int))
runApp app = runExceptT (runStateT (runReaderT app (Config 2)) 0)

main :: IO ()
main = runApp (mapM_ attempt ["a", "b", "c"]) >>= print
-- Attempt 1: a / Attempt 2: b / Left "too many attempts"

26.2 mtl-Style Constraints

MonadReader (ask, asks, local) MonadState (get, put, modify, gets) MonadWriter (tell)

MonadError (throwError, catchError) Failure without information MonadIO (liftIO)

Order matters: StateT s (ExceptT e m) loses the state on error; ExceptT e (StateT s m) keeps it.

Alternatives: ReaderT-over-IO pattern (Module 51), effect systems like effectful, polysemy, fused-effects, bluefin (Module 42).

{-# LANGUAGE FlexibleContexts #-}
-- Write functions against capabilities, not concrete stacks
incrementCounter :: (MonadState Int m, MonadIO m) => m ()
incrementCounter = do
  modify (+ 1)
  n <- get
  liftIO (print n)

validate :: MonadError String m => Int -> m Int
validate x = if x < 0 then throwError "negative" else pure x

-- Works in any stack providing those capabilities