mtl-Style Classes
Prerequisites: Monad transformers
The mtl library defines a type class for each effect —
MonadState, MonadReader, MonadError, MonadWriter, MonadIO. Instead
of naming a concrete stack, a function states only the capabilities
it needs:
import Control.Monad.State
import Control.Monad.Except
import Control.Monad (when, replicateM)
nextTicket :: (MonadState Int m, MonadError String m) => m Int
nextTicket = do
n <- get
when (n >= 3) $ throwError "sold out"
put (n + 1)
return (n + 1)
buyTickets :: (MonadState Int m, MonadError String m) => Int -> m [Int]
buyTickets k = replicateM k nextTicket
main :: IO ()
main = do
print (runExcept (runStateT (buyTickets 2) 0))
print (runExcept (runStateT (buyTickets 5) 0))
print (runStateT (buyTickets 2) 0 :: Either String ([Int], Int))
Right ([1,2],2)
Left "sold out"
Right ([1,2],2)
nextTicketworks in any monad that hasIntstate andStringerrors — the caller picks the concrete stack.- The last line runs the same code with
Either Stringas the base monad instead ofExcept. - The signature documents exactly which effects the function uses. A
function with only
MonadState Int mprovably can't throw errors or do I/O.
Your own capability classes
The same idea makes IO code testable. Define a class for the effect, implement it for IO in production, and for a pure monad in tests:
import Control.Monad.State
class Monad m => MonadLogger m where
logMsg :: String -> m ()
instance MonadLogger IO where
logMsg s = putStrLn ("[log] " ++ s)
newtype TestM a = TestM (State [String] a)
deriving (Functor, Applicative, Monad)
instance MonadLogger TestM where
logMsg s = TestM (modify (++ [s]))
runTest :: TestM a -> (a, [String])
runTest (TestM m) = runState m []
processOrder :: MonadLogger m => Int -> m Int
processOrder qty = do
logMsg ("processing " ++ show qty)
let total = qty * 3
logMsg ("total " ++ show total)
return total
main :: IO ()
main = do
_ <- processOrder 4
print (runTest (processOrder 4))
[log] processing 4
[log] total 12
(12,["processing 4","total 12"])
deriving (Functor, Applicative, Monad) on a newtype uses the
GeneralizedNewtypeDeriving extension (part of GHC2021) to reuse
State's instances.
Alternatives
mtl style is the most common approach, but you'll also meet effect
systems — libraries such as effectful, polysemy, and bluefin —
which let you define and combine effects more freely, and the plain
ReaderT pattern from the previous lesson. All three are used in
production; learn mtl first, since the others build on the same ideas.