Polyglot
Languages Haskell monads
Haskell § monads

Monads

Monads are Haskell’s central abstraction for sequential computation. A monad is a type constructor m that admits two principal operations — pure (or return) and >>= (pronounced bind) — such that a sequence of m-typed computations may be composed, with each step’s result feeding into the next. The mechanism is general: IO, Maybe, [], Either, State, Reader, Writer, STM, and dozens of others are monads, and the monad operations admit a single notation (do-notation) that works across all of them. Understanding monads is necessary to write non-trivial Haskell; the principal hurdle for newcomers is conceptual — once the pattern clicks, monads become the standard idiom for sequencing computation.

This page covers the Monad type class, the principal monads, do-notation, monad transformers (briefly), and the conventions for using each.

The Monad type class

class Applicative m => Monad m where
    return :: a -> m a            -- = pure (since AMP)
    (>>=)  :: m a -> (a -> m b) -> m b
    (>>)   :: m a -> m b -> m b
    m >> k = m >>= \_ -> k

The two operations:

  • return (or pure) — wrap a value in the monad.
  • >>= (bind) — given a monadic value m a and a function a -> m b, produce a monadic value m b.

The intuition: >>= admits sequencing — the result of one computation flows into the next. Each step’s choice of computation depends on the previous step’s result.

>> is sequencing without using the result — useful when the first action’s value is () (or otherwise uninteresting).

The Monad laws

Every Monad instance must satisfy:

-- Left identity:
return x >>= f = f x

-- Right identity:
m >>= return = m

-- Associativity:
(m >>= f) >>= g = m >>= (\x -> f x >>= g)

The laws ensure monads compose predictably; the compiler does not check the laws, but a non-law-abiding monad produces surprising behaviour.

do-notation

Direct use of >>= produces nested-lambda code:

greet :: IO ()
greet = getLine >>= \name -> putStrLn ("Hello, " ++ name)

readTwoNumbers :: IO Int
readTwoNumbers =
    getLine >>= \s1 ->
    getLine >>= \s2 ->
    return (read s1 + read s2)

The do-notation is syntactic sugar for >>=:

greet :: IO ()
greet = do
    name <- getLine
    putStrLn ("Hello, " ++ name)

readTwoNumbers :: IO Int
readTwoNumbers = do
    s1 <- getLine
    s2 <- getLine
    return (read s1 + read s2)

The do-block contains a sequence of statements:

  • <expr> — evaluate, discard the result.
  • x <- <expr> — evaluate, bind the result to x.
  • let x = <expr> — pure binding.

The compiler translates each form to the corresponding >>= chain. The do-notation makes monadic code read like a sequence of actions.

The block’s type is the monadic type of its last expression:

do
    putStrLn "starting"
    line <- getLine
    return (length line)
-- :: IO Int

let in do

let admits pure bindings within a do-block:

main = do
    line <- getLine
    let upper = map toUpper line
        len   = length upper
    putStrLn upper
    print len

The let is the same as let … in … but without the in; the bindings are scoped to the rest of the do-block.

The Maybe monad

Maybe is the simplest substantive monad:

instance Monad Maybe where
    return = Just
    Nothing >>= _ = Nothing
    Just x  >>= f = f x

The intuition: a sequence of Maybe-typed computations short-circuits on Nothing. Each step that produces Nothing propagates it through the rest:

parseAge :: String -> Maybe Int
parseAge = readMaybe

parsePerson :: String -> String -> Maybe Person
parsePerson name ageStr = do
    age <- parseAge ageStr
    return (Person name age)

-- Without do-notation:
parsePerson name ageStr = parseAge ageStr >>= \age -> return (Person name age)

-- Or with Applicative:
parsePerson name ageStr = Person name <$> parseAge ageStr

The Maybe monad is the conventional choice for “a sequence of operations any of which may fail with no detail”.

The Either e monad

Either e carries an error value:

instance Monad (Either e) where
    return = Right
    Left e   >>= _ = Left e
    Right x  >>= f = f x

The intuition: a sequence of Either e-typed computations short-circuits on Left, propagating the error:

parseAge :: String -> Either String Int
parseAge s = case reads s of
    [(n, "")] -> Right n
    _         -> Left ("could not parse age: " ++ s)

validateAge :: Int -> Either String Int
validateAge n
    | n < 0     = Left "age must be non-negative"
    | n > 150   = Left "age must be reasonable"
    | otherwise = Right n

parsePerson :: String -> String -> Either String Person
parsePerson name ageStr = do
    rawAge <- parseAge ageStr
    age    <- validateAge rawAge
    return (Person name age)

The Either monad is the conventional choice for “a sequence of operations any of which may fail with a specific error”.

The list monad

The list monad represents non-determinism:

instance Monad [] where
    return x = [x]
    xs >>= f = concatMap f xs        -- = concat (map f xs)

The intuition: a [a]-typed computation may return any number of as; bind admits “for each result, do the next computation”:

pairs :: [(Int, Int)]
pairs = do
    x <- [1, 2, 3]
    y <- [10, 20]
    return (x, y)
-- [(1,10), (1,20), (2,10), (2,20), (3,10), (3,20)]

pythagorean :: Int -> [(Int, Int, Int)]
pythagorean n = do
    a <- [1..n]
    b <- [a..n]
    c <- [b..n]
    if a * a + b * b == c * c
        then return (a, b, c)
        else []

The list monad subsumes much of what list comprehensions provide:

[(x, y) | x <- [1..3], y <- [10, 20]]
-- equivalent to the do-notation pairs above

The list comprehension is conventionally clearer for simple cases; the do-form admits more elaborate logic.

The IO monad

IO is the principal monad for effectful computation. The full treatment is in IO.

The conventional IO patterns:

main :: IO ()
main = do
    putStrLn "What is your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

IO is also a monad in the formal sense — the >>= and return operations satisfy the monad laws. The mechanism that makes IO distinctive is the type system: a value of type IO a cannot be examined except in another IO context, so the type system tracks effectful code separately from pure code.

The State monad

The State monad threads state through a sequence of operations without explicit parameters:

import Control.Monad.State

newtype State s a = State { runState :: s -> (a, s) }

-- The monad operations:
get :: State s s
put :: s -> State s ()
modify :: (s -> s) -> State s ()
gets :: (s -> a) -> State s a
runState :: State s a -> s -> (a, s)
evalState :: State s a -> s -> a       -- discard the final state
execState :: State s a -> s -> s       -- discard the final result

A State-monad computation:

counter :: State Int Int
counter = do
    n <- get
    put (n + 1)
    return n

main = do
    let (results, finalState) = runState (replicateM 5 counter) 0
    print results        -- [0, 1, 2, 3, 4]
    print finalState     -- 5

The mechanism is the conventional Haskell substitute for mutable variables in pure code; the threading of the state is handled by the monad.

The Reader monad

Reader admits accessing a shared environment without explicit parameters:

import Control.Monad.Reader

newtype Reader r a = Reader { runReader :: r -> a }

ask :: Reader r r
asks :: (r -> a) -> Reader r a
local :: (r -> r) -> Reader r a -> Reader r a
runReader :: Reader r a -> r -> a

A Reader-monad computation:

data Config = Config { port :: Int, host :: String }

makeUrl :: Reader Config String
makeUrl = do
    config <- ask
    return ("http://" ++ host config ++ ":" ++ show (port config))

main = do
    let config = Config { port = 8080, host = "localhost" }
    putStrLn (runReader makeUrl config)

The mechanism admits dependency injection without explicit parameters.

The Writer monad

Writer admits accumulating an output value:

import Control.Monad.Writer

newtype Writer w a = Writer { runWriter :: (a, w) }

tell :: Monoid w => w -> Writer w ()
runWriter :: Writer w a -> (a, w)
execWriter :: Writer w a -> w           -- discard the result

A Writer-monad computation:

fizzbuzz :: Int -> Writer [String] ()
fizzbuzz n = do
    when (n `mod` 3 == 0) (tell ["fizz"])
    when (n `mod` 5 == 0) (tell ["buzz"])

main = do
    let (_, log) = runWriter (mapM_ fizzbuzz [1..15])
    mapM_ putStrLn log

The mechanism admits accumulating logs, traces, or any other value with a Monoid instance. The Writer is generally considered the least useful of the standard monads — it is space-leak-prone and there are usually better alternatives.

The STM monad

STM (Software Transactional Memory) admits atomic concurrent updates:

import Control.Concurrent.STM

newTVar :: a -> STM (TVar a)
readTVar :: TVar a -> STM a
writeTVar :: TVar a -> a -> STM ()
atomically :: STM a -> IO a

The full treatment is in Concurrency. The principal point: STM is a monad in its own right, with its own operations that compose; the atomically function bridges from STM to IO.

Monad transformers

The single-monad approach is sometimes insufficient; programs may need both State and IO, or both Reader and Either. Monad transformers combine monads into a single richer monad:

import Control.Monad.State
import Control.Monad.Trans

-- StateT s IO a is "IO with State"
counterIO :: StateT Int IO ()
counterIO = do
    n <- get
    liftIO (putStrLn ("Counter: " ++ show n))
    put (n + 1)

main = do
    finalState <- execStateT (replicateM_ 5 counterIO) 0
    print finalState

The principal transformers:

TransformerEffect
MaybeT mAdds Maybe (failure) to m
ExceptT e mAdds Either e (errors) to m
StateT s mAdds state to m
ReaderT r mAdds environment to m
WriterT w mAdds accumulator to m
IdentityT mNo effect; only useful with transformer stacks

The mtl package provides the conventional set. The MonadIO constraint admits running IO actions in any transformer stack containing IO:

class Monad m => MonadIO m where
    liftIO :: IO a -> m a

liftIO admits using putStrLn and other IO actions in a StateT or ReaderT context.

The full treatment of monad transformers is a substantial topic; the conventional Haskell idiom in modern libraries is the mtl-style class-based approach (MonadState, MonadReader, MonadIO) rather than the explicit transformer-stack form.

MonadFail

class Monad m => MonadFail m where
    fail :: String -> m a

MonadFail is the class for monads that admit pattern-match failure. IO’s fail throws an exception; Maybe’s produces Nothing; []’s produces [].

The class admits using non-trivial patterns in do-notation:

processFirst :: [Int] -> Maybe Int
processFirst xs = do
    Just n <- listToMaybe xs    -- pattern match; calls fail on Nothing
    return n

If the pattern fails, the monad’s fail is invoked. The mechanism is occasionally surprising; the conventional advice is to use explicit case for non-trivial patterns in do.

Common patterns

Sequencing with >>=

import Network.HTTP.Simple

fetchAndParse :: String -> IO (Maybe Json)
fetchAndParse url = do
    response <- httpJSON (parseRequest_ url)
    return (Just (getResponseBody response))

The pattern is direct sequencing: each step’s result feeds the next.

Error short-circuiting with Either

processOrder :: Input -> Either String Result
processOrder input = do
    raw <- parseInput input
    valid <- validateOrder raw
    enriched <- enrichOrderData valid
    return (computeResult enriched)

The Either String chain produces “the first error” or the final result.

Stateful computation with State

runProcess :: [Item] -> State Stats ()
runProcess items = do
    forM_ items $ \item -> do
        stats <- get
        put (updateStats item stats)

The mechanism admits explicit state without mutable variables.

Combining multiple monads with mtl-style classes

import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.IO.Class

processItem :: (MonadState Stats m, MonadReader Config m, MonadIO m) => Item -> m ()
processItem item = do
    config <- ask
    when (verbose config) $ do
        liftIO (putStrLn ("Processing " ++ itemName item))
    modify (\s -> updateStats item s)

The mtl-style constraints admit polymorphic monadic code that works in any stack containing the required effects.

Applicative when the steps are independent

-- Use Applicative when the next step doesn't depend on the previous:
combine :: Maybe Int -> Maybe Int -> Maybe Int
combine = liftA2 (+)
-- or:
combine x y = (+) <$> x <*> y

-- Use Monad when there's a dependency:
divide :: Maybe Int -> Maybe Int -> Maybe Int
divide x y = do
    n <- x
    d <- y
    if d == 0 then Nothing else Just (n `div` d)

The conventional advice: use Applicative if possible; reach for Monad when the next step depends on the previous.

A note on the conventional Haskell discipline

The full Functor/Applicative/Monad hierarchy is the foundation of Haskell’s effect typing:

  • Functor — map over a structure.
  • Applicative — combine independent effectful values with a pure function.
  • Monad — sequence effectful computations where each depends on the previous.

Most Haskell code does not write monad transformers explicitly; the mtl package provides class-based abstractions that admit polymorphic effectful code without the syntactic overhead. Modern Haskell libraries increasingly use effect systemspolysemy, freer-simple, effectful, cleff — that admit even more flexible composition of effects than the mtl-style monad transformers.

The principal advice for new Haskell code:

  • Use do-notation for monadic code; reach for the underlying >>= only when it is clearer.
  • Use Maybe for “may fail with no detail”; Either e for “may fail with detail”.
  • Use IO only when actually performing I/O; pure code should remain in pure types.
  • Use State or mtl-style class abstractions for stateful computations.
  • Reach for monad transformers when the program needs multiple effects simultaneously.
  • For complex effect compositions, consider an effect system rather than hand-stacking transformers.