Skip to content
Monad Transformers
step 1/7

Reading — step 1 of 7

Learn

~3 min readMonad Transformers, GADTs, Concurrency

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 ExceptT
  • liftEither — lift a pure Either result
  • throwError — raise an error
  • runExceptT — 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 composition
  • effectful — 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 need lift to push them up the stack. mtl-style classes hide this.
  • liftIO everywhere 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…