Monads and Functors
The Monad Concept
Section titled “The Monad Concept”A monad is a design pattern for structuring computations. In Haskell, a monad wraps a value in a computational context that defines how operations chain together. Each monad provides its own rules for sequencing and combining computations.
Formally, a monad is any type that implements:
class Applicative m => Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m bThe three monad laws ensure predictable behavior:
- Left identity:
return x >>= fis the same asf x - Right identity:
m >>= returnis the same asm - Associativity:
(m >>= f) >>= gis the same asm >>= (\x -> f x >>= g)
Maybe Monad
Section titled “Maybe Monad”The Maybe monad represents computations that might fail. Nothing represents failure, and Just a wraps a successful result. The bind operator >>= short-circuits on Nothing:
-- Database lookup style chaindata User = User { userId :: Int, userName :: String } deriving (Show)
users :: [(Int, User)]users = [(1, User 1 "Alice"), (2, User 2 "Bob")]
orders :: [(Int, String)]orders = [(1, "Book"), (2, "Laptop")]
lookupUser :: Int -> Maybe UserlookupUser n = lookup n users
lookupOrder :: String -> Maybe StringlookupOrder n = lookup n orders
-- Chaining Maybe computations with >>=getUserOrder :: Int -> Maybe StringgetUserOrder userId" = do user <- lookupUser userId' order <- lookupOrder (userName user) return order
-- If lookupUser returns Nothing, the whole chain returns Nothing-- If lookupOrder returns Nothing, the whole chain returns Nothing-- Both must succeed to produce a resultPractical Maybe Patterns
Section titled “Practical Maybe Patterns”-- Safe headsafeHead :: [a] -> Maybe asafeHead [] = NothingsafeHead (x:_) = Just x
-- Chain of safe operationsprocessList :: [Int] -> Maybe IntprocessList xs = do first <- safeHead xs second <- safeHead (drop 1 xs) return (first + second)
-- Using Maybe in validation pipelinesvalidateAge :: Int -> Maybe IntvalidateAge n | n >= 0 && n <= 150 = Just n | otherwise = Nothing
validateName :: String -> Maybe StringvalidateName s | length s >= 1 = Just s | otherwise = Nothing
createProfile :: String -> Int -> Maybe (String, Int)createProfile name age = do validName <- validateName name validAge <- validateAge age return (validName, validAge)Either Monad
Section titled “Either Monad”Either e a represents a value that is either Left e (an error of type e) or Right a (a successful result). Unlike Maybe, it carries information about what went wrong:
-- Using Either for detailed error reportingdata ParseError = UnexpectedChar Char | UnexpectedEOF | InvalidNumber String deriving (Show)
parseDigit :: Char -> Either ParseError IntparseDigit c | c >= '0' && c <= '9' = Right (ord c - ord '0') | otherwise = Left (UnexpectedChar c)
parseNumber :: String -> Either ParseError IntparseNumber [] = Left UnexpectedEOFparseNumber (c:cs) = do d <- parseDigit c return d
-- Either monad: Left short-circuits, Right continuesmultiStep :: Either String IntmultiStep = do a <- Right 10 b <- Left "step 2 failed" c <- Right 30 return (a + b + c)-- => Left "step 2 failed"Either vs Maybe
Section titled “Either vs Maybe”-- Maybe: no information about failuresafeDiv :: Int -> Int -> Maybe IntsafeDiv _ 0 = NothingsafeDiv x y = Just (x `div` y)
-- Either: detailed error informationsafeDiv :: Int -> Int -> Either String IntsafeDiv _ 0 = Left "division by zero"safeDiv x y = Right (x `div` y)
-- Choose Maybe when you just need success/failure-- Choose Either when you need to report what went wrongIO Monad
Section titled “IO Monad”The IO monad performs side effects while maintaining referential transparency. An IO a action describes a computation that, when executed, produces a value of type a while potentially interacting with the outside world.
-- Basic IO operationsmain :: IO ()main = do -- putStrLn :: String -> IO () putStrLn "Enter a number:" -- getLine :: IO String line <- getLine -- read :: Read a => String -> a let n = read line :: Int -- print :: Show a => a -> IO () print (n * n)
-- IO is lazy: actions are not executed until main runs-- An IO action is just a description of what to dodescribe :: IO ()describe = putStrLn "This does nothing until executed in main"IO Refs for Local Mutable State
Section titled “IO Refs for Local Mutable State”When you truly need mutable state, IORef provides it within the IO monad:
import Data.IORef
counterExample :: IO IntcounterExample = do counter <- newIORef 0 modifyIORef counter (+1) modifyIORef counter (+1) modifyIORef counter (+1) readIORef counter-- => 3
-- atomicModifyIORef combines read-modify-write atomicallyincrementAndReturn :: IORef Int -> IO IntincrementAndReturn ref = atomicModifyIORef ref (\n -> (n + 1, n))-- Returns the OLD value while incrementingState Monad
Section titled “State Monad”The State monad threads state through a computation without explicit state passing. The type State s a represents a computation that takes a state of type s and produces a result of type a along with an updated state.
import Control.Monad.State
-- State s a = s -> (a, s)
type Stack = [Int]
-- Push: add element to top of stackpush :: Int -> State Stack ()push x = modify (x:)
-- Pop: remove and return top elementpop :: State Stack Intpop = do stack <- get case stack of [] -> error "stack underflow" (x:xs) -> put xs >> return x
-- peek: look at top element without removing itpeek :: State Stack Intpeek = gets head
-- Composing State operationsstackManip :: State Stack IntstackManip = do push 3 push 5 a <- pop push 7 b <- pop return (a + b)
-- Running the State computationrunStack :: (Int, Stack)runStack = runState stackManip [1, 2]-- => (12, [1, 2, 3, 7])
-- runState: returns (result, finalState)-- evalState: returns only the result-- execState: returns only the final stateState Monad Internals
Section titled “State Monad Internals”-- The State monad is directly a newtype over a functionnewtype State s a = State { runState :: s -> (a, s) }
instance Functor (State s) where fmap f (State g) = State $ \s -> let (a, s') = g s in (f a, s')
instance Applicative (State s) where pure a = State $ \s -> (a, s) State f <*> State g = State $ \s -> let (h, s') = f s (a, s'') = g s' in (h a, s'')
instance Monad (State s) where return = pure State f >>= g = State $ \s -> let (a, s') = f s in runState (g a) s'Practical State Usage
Section titled “Practical State Usage”-- Word counting with Statetype WordMap = [(String, Int)]
addWord :: String -> State WordMap ()addWord word = modify (insertWord word) where insertWord w [] = [(w, 1)] insertWord w ((k, c):rest) | w == k = (k, c + 1) : rest | otherwise = (k, c) : insertWord w rest
countWords :: String -> WordMapcountWords = execState (mapM_ addWord (words text)) []
-- Game loop with Statetype GameState = (Int, Int) -- (playerX, playerY)
move :: String -> State GameState ()move "left" = modify (\(x, y) -> (x - 1, y))move "right" = modify (\(x, y) -> (x + 1, y))move "up" = modify (\(x, y) -> (x, y + 1))move "down" = modify (\(x, y) -> (x, y - 1))move _ = return ()
runMoves :: [String] -> GameStaterunMoves = execState (mapM_ move) (0, 0)Reader Monad
Section titled “Reader Monad”The Reader monad provides access to a shared read-only environment. It is useful for configuration, dependency injection, and computations that need access to a common context:
import Control.Monad.Reader
-- Reader r a = r -> a
-- Configuration typetype AppConfig = String -- database connection string
-- ask retrieves the environmentgetDbConfig :: Reader AppConfig StringgetDbConfig = ask
-- local runs a computation with a modified environmentwithConfig :: Reader AppConfig a -> String -> Reader AppConfig awithConfig = local . const
-- Practical example: web application contexttype App = Reader AppEnv
data AppEnv = AppEnv { envDbConn :: String , envLogger :: String -> IO () , envPort :: Int }
handleRequest :: String -> App StringhandleRequest path = do env <- ask -- can access envDbConn, envLogger, envPort return ("Handling: " ++ path)
runApp :: AppEnv -> App a -> arunApp = runReaderReader Combinators
Section titled “Reader Combinators”-- Combining Reader computationscombined :: Reader Int Intcombined = do env1 <- ask env2 <- local (+10) ask env3 <- local (*2) ask return (env1 + env2 + env3)
-- runReader combined 5 => 5 + 15 + 10 = 30Writer Monad
Section titled “Writer Monad”The Writer monad accumulates a log or auxiliary output alongside a computation result:
import Control.Monad.Writerimport Data.Monoid (Sum(..))
-- Writer w a = (a, w)
-- A computation that logs its stepsfactorialLog :: Integer -> Writer [String] IntegerfactorialLog 0 = do tell ["Base case: 0! = 1"] return 1factorialLog n = do tell ["Computing " ++ show n ++ "!"] prev <- factorialLog (n - 1) let result = n * prev tell [show n ++ "! = " ++ show result] return result
-- Running the WriterrunFactorial :: (Integer, [String])runFactorial = runWriter (factorialLog 5)-- => (120, ["Computing 5!", "Computing 4!", ..., "Base case: 0! = 1"])Writer with Different Monoids
Section titled “Writer with Different Monoids”-- Sum monoid: accumulates numberssumWriter :: Writer (Sum Int) ()sumWriter = do tell (Sum 10) tell (Sum 20) tell (Sum 30)
runSum :: Sum IntrunSum = execWriter sumWriter -- => Sum 30
-- Product monoid: accumulates productsproductWriter :: Writer (Product Int) ()productWriter = do tell (Product 2) tell (Product 3) tell (Product 4)
runProduct :: Product IntrunProduct = execWriter productWriter -- => Product 24Monad Transformers
Section titled “Monad Transformers”Why Transformers?
Section titled “Why Transformers?”Different monads solve different problems: Maybe handles failure, State handles mutable state, Reader handles shared environment, IO handles side effects. Real applications often need multiple effects simultaneously. Monad transformers stack monads to combine their capabilities.
MaybeT
Section titled “MaybeT”MaybeT m a wraps a computation in monad m that may fail:
import Control.Monad.Trans.Maybeimport Control.Monad.Trans.Class (lift)
-- MaybeT IO Int: an IO computation that may faillookupUserInDb :: String -> MaybeT IO UserlookupUserInDb name = MaybeT $ do -- Perform IO to check the database result <- dbQuery ("SELECT * FROM users WHERE name = " ++ name) return (parseUser result)
getUserOrders :: String -> MaybeT IO [Order]getUserOrders userName = do user <- lookupUserInDb userName orders <- MaybeT (fetchOrders (userId user)) return orders
-- Running MaybeThandleUsers :: IO ()handleUsers = do result <- runMaybeT (getUserOrders "Alice") case result of Nothing -> putStrLn "User not found or no orders" Just ords -> print ordsStateT
Section titled “StateT”StateT s m a adds state to any underlying monad:
import Control.Monad.Trans.State
-- StateT Int IO () combines State Int with IOgameLoop :: StateT Int IO ()gameLoop = do score <- get lift (putStrLn ("Current score: " ++ show score)) lift (putStrLn "Enter a number:") input <- lift getLine let n = read input :: Int modify (+ n) score' <- get lift (putStrLn ("New score: " ++ show score')) when (score' >= 100) (lift (putStrLn "You win!"))
runGame :: IO ()runGame = evalStateT gameLoop 0ExceptT
Section titled “ExceptT”ExceptT e m a is the transformer version of Either e:
import Control.Monad.Trans.Except
type AppM = ExceptT String IO
validateUser :: String -> AppM UservalidateUser name | null name = throwError "Name cannot be empty" | length name > 50 = throwError "Name too long" | otherwise = lift (fetchUser name)
createAccount :: String -> Int -> AppM AccountcreateAccount name age = do user <- validateUser name when (age < 18) (throwError "Must be 18 or older") lift (saveAccount user age)
-- Running ExceptTrunAppM :: AppM a -> IO (Either String a)runAppM = runExceptT
main :: IO ()main = do result <- runAppM (createAccount "" 25) case result of Left err -> putStrLn ("Error: " ++ err) Right acc -> print accAlternative and MonadPlus
Section titled “Alternative and MonadPlus”Alternative
Section titled “Alternative”Alternative provides a choice operation for Applicative functors:
class Applicative f => Alternative f where empty :: f a (<|>) :: f a -> f a -> f a
-- MaybeNothing <|> Just 5 -- => Just 5Just 3 <|> Just 5 -- => Just 3Nothing <|> Nothing -- => Nothing
-- List[] <|> [1, 2] -- => [1, 2][1] <|> [2] -- => [1][1] <|> [2, 3] -- => [1, 2, 3]
-- guard: short-circuits with emptyguard :: Alternative f => Bool -> f ()guard True = pure ()guard False = empty
-- Using guard in list comprehensionsevens = do x <- [1..10] guard (even x) return x-- => [2, 4, 6, 8, 10]MonadPlus
Section titled “MonadPlus”MonadPlus provides the same operations for Monads:
class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a
-- The relationship: MonadPlus m implies Alternative m-- mzero = empty, mplus = (<|>)
-- Using msum: find the first successful computationlookup1 :: k -> Map k v -> Maybe vlookup2 :: k -> Map k v -> Maybe v
findValue :: k -> Maybe vfindValue k = msum [lookup1 k map1, lookup2 k map2]Kleisli Composition
Section titled “Kleisli Composition”Kleisli composition (<=< and >=>) composes monadic functions:
-- Kleisli arrow: a -> m b-- (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)-- (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
import Control.Monad ( (>=>), (<=<) )
-- Composition of monadic functionssafeDiv :: Int -> Int -> Maybe IntsafeDiv _ 0 = NothingsafeDiv x y = Just (x `div` y)
half :: Int -> Maybe Inthalf x = safeDiv x 2
compute :: Int -> Maybe Intcompute = half >=> half >=> half-- compute 16 => Just 2 (16 / 2 / 2 / 2)-- compute 7 => Nothing (7 / 2 = Nothing)
-- Pipe style with >=> pipelineprocess :: Int -> Maybe Intprocess = validateAge >=> \age -> computeDiscount age >=> applyDiscountJoin and Bind
Section titled “Join and Bind”join and >>= are fundamental operations that can each be expressed in terms of the other:
-- join :: Monad m => m (m a) -> m a-- Flattens one layer of the monad
join :: Monad m => m (m a) -> m ajoin mmx = mmx >>= id
-- bind in terms of join-- m >>= f = join (fmap f m)
-- Examples with Maybejoin (Just (Just 5)) -- => Just 5join (Just Nothing) -- => Nothingjoin Nothing -- => Nothing
-- Examples with Listjoin [[1, 2], [3, 4]] -- => [1, 2, 3, 4]join [[], [1, 2]] -- => [1, 2]join [] -- => []
-- Examples with IOjoin (getLine >>= \x -> return (print x))-- Reads a line, then prints itMonad Laws
Section titled “Monad Laws”Understanding the monad laws ensures that monadic code behaves predictably:
-- Law 1: Left Identity-- return x >>= f == f xleftIdentity :: Maybe IntleftIdentity = do let f x = Just (x + 1) result1 = return 5 >>= f -- Just 6 result2 = f 5 -- Just 6 -- result1 == result2
-- Law 2: Right Identity-- m >>= return == mrightIdentity :: Maybe IntrightIdentity = do let m = Just 5 result1 = m >>= return -- Just 5 result2 = m -- Just 5
-- Law 3: Associativity-- (m >>= f) >>= g == m >>= (\x -> f x >>= g)associativity :: Maybe Intassociativity = do let m = Just 5 f x = Just (x + 1) g x = Just (x * 2) result1 = (m >>= f) >>= g -- Just 12 result2 = m >>= (\x -> f x >>= g) -- Just 12Choosing the Right Monad
Section titled “Choosing the Right Monad”| Monad | Use Case | When to Use |
|---|---|---|
Maybe | Optional values | Computations that may fail with no details |
Either e | Error handling | Computations that fail with error messages |
IO | Side effects | Any interaction with the outside world |
State s | Mutable state | Thread state through pure computations |
Reader r | Shared config | Read-only environment passed implicitly |
Writer w | Logging | Accumulate log messages alongside results |
[] | Non-determinism | Multiple possible results |
Identity | No effect | The simplest monad, wraps a plain value |
Intuition
Section titled “Intuition”A monad is a conveyor belt in a factory: Imagine a factory where each station can transform the item on the belt, but the belt itself has special properties. The Maybe belt drops items into the void if anything goes wrong — no downstream station ever sees a broken part. The IO belt logs every operation — the belt is the audit trail. The State belt carries a clipboard that accumulates instructions as items move along. >>= (bind) is the instruction: “take the item off the belt, run it through this station, and put the result back on.”
Why it matters: Monads let you compose computations with effects (failure, state, I/O) in a purely functional way. Instead of side effects happening invisibly, the monad type tells you what kind of effects a function has — Maybe a might fail, IO a does I/O, State s a mutates state.
The key insight: The monad laws (left identity, right identity, associativity) guarantee that chaining operations behaves predictably — you can refactor m >>= f >>= g into m >>= (\x -> f x >>= g) without changing the result.
Combining Effects
Section titled “Combining Effects”In practice, applications combine multiple monad transformers:
import Control.Monad.Readerimport Control.Monad.Stateimport Control.Monad.Exceptimport Control.Monad.Trans.Class (lift)
-- Application monad stacktype AppM = ReaderT Config (StateT AppState (ExceptT AppError IO))
-- In this stack (from outside in):-- ReaderT Config: read-only configuration-- StateT AppState: mutable application state-- ExceptT AppError: error handling-- IO: actual side effects
runApp :: Config -> AppState -> AppM a -> IO (Either AppError a)runApp config state action = runExceptT (evalStateT (runReaderT action config) state)Cross-References
Section titled “Cross-References”- Types and Functions: Type classes and function composition underlying functor and monad operations.
- Pattern Matching: Pattern matching on Maybe, Either, and list constructors in monadic code.
- Advanced Types: Type families and GADTs used to implement custom monad transformers.
Common Mistakes
Section titled “Common Mistakes”Confusing fmap with >>=: fmap applies a pure function inside a functor; >>= applies a function that itself produces a functor. Using fmap when you need >>= leads to nested functors (f (f a)) instead of flattened results.
Forgetting that return is not a constructor: return is just pure — it wraps a value in the monad. It doesn’t escape the monad or perform IO. Thinking return exits the monad is a common misconception from imperative languages.
Stacking monad transformers in the wrong order: The order of ReaderT, StateT, and ExceptT determines which effects are innermost. Wrong ordering makes lift calls verbose or impossible. Put the most-used transformer closest to the inner monad.