Reading — step 1 of 7
Learn
Real Haskell code combines effects: a function might do IO AND track state AND return Either an error. Monad transformers stack these effects. Real World Haskell devotes a chapter; the Haskell School of Music uses them throughout.
The problem
doStuff :: IO (Either String Int)
doStuff = do
raw <- readFile "input.txt" -- IO String
case parseInput raw of -- Either String [Int]
Left err -> return (Left err)
Right xs -> case validate xs of
Left err -> return (Left err)
Right valid -> return (Right (sum valid))
Nested Maybe/Either inside IO becomes painful — repeating case matching at every step, hand-threading the error.
ExceptT — Either inside IO
import Control.Monad.Except
doStuff :: ExceptT String IO Int
doStuff = do
raw <- liftIO (readFile "input.txt")
xs <- liftEither (parseInput raw)
valid <- liftEither (validate xs)
return (sum valid)
-- Run it:
result <- runExceptT doStuff -- IO (Either String Int)
ExceptT e m a = monad transformer adding Either-style error handling on top of any monad m. With m = IO, you get IO + early-return-on-error.
liftIO— lift an IO action into ExceptTliftEither— lift a pure Either resultthrowError— raise an errorrunExceptT— peel off the transformer to get IO (Either e a)
Common transformers
MaybeT m a -- Maybe inside m — early return on Nothing
ExceptT e m a -- Either e inside m — typed errors
ReaderT r m a -- read-only environment
StateT s m a -- mutable state
WriterT w m a -- log/accumulate
Stack them: ReaderT Config (StateT GameState IO) a — config-aware, stateful, in IO. The order matters — outer transformer wraps inner.
A typical app stack
type App = ReaderT Config (LoggingT IO)
runApp :: Config -> App a -> IO a
runApp cfg = runStdoutLoggingT . flip runReaderT cfg
getUser :: UserId -> App User
getUser uid = do
cfg <- ask -- ReaderT: get config
logInfoN ("fetching user " <> tshow uid)
liftIO (fetchUser cfg uid) -- IO: do the work
App is a Reader-on-top-of-Logging-on-top-of-IO. Every function in the app threads config, logging, and IO without the caller writing do-blocks of boilerplate.
mtl-style: typeclasses for effects
The mtl package provides typeclass versions:
fetchAndLog :: (MonadReader Config m, MonadIO m) => UserId -> m User
fetchAndLog uid = do
cfg <- ask
liftIO (fetchUser cfg uid)
The constraint says "any monad m that has Reader Config and IO capabilities." Concrete stack chooses transformers; the function works with any matching choice.
This is mtl-style: write functions against typeclass capabilities, not concrete stacks. Decouples your code from the exact effect arrangement.
Modern alternatives
Monad transformers are a heavy abstraction. Some libraries try to do it differently:
fused-effects/polysemy— algebraic effects, more flexible compositioneffectful— newer, simpler approach to effect tracking- ReaderT pattern — just one ReaderT for everything; resist deep stacks
For most production Haskell, simple ReaderT-over-IO is enough. Reach for full transformer stacks when you have multiple genuinely-distinct effects.
Common mistakes
- Stacking transformers without need — start with IO, add ReaderT for config, only add more if pain warrants.
- Forgetting
lift— operations from inner monads needliftto push them up the stack. mtl-style classes hide this. liftIOeverywhere vs cleaner mtl constraints — for simple stacks, lift is fine; for complex ones, prefer constraint-based code.- Wrong transformer order — ExceptT IO and IO ExceptT have different short-circuit behavior. Read carefully.
- Skipping monad transformers entirely — for non-trivial effectful code, you'll re-invent them. Better to learn the standard tools.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…