Monad morphisms commonly arise when manipulating existing monad transformer
code for compatibility purposes. The MFunctor, MonadTrans, and
MMonad classes define standard ways to change monad transformer stacks:
lift introduces a new monad transformer layer of any type.
squash flattens two identical monad transformer layers into a single
layer of the same type.
hoist maps monad morphisms to modify deeper layers of the monad
transformer stack.
The class of monad transformers.
For any monad m, the result t m should also be a monad,
and lift should be a monad transformation from m to t m,
i.e. it should satisfy the following laws:
Since 0.6.0.0 and for GHC 8.6 and later, the requirement that t m
be a Monad is enforced by the implication constraint
forall m. Monad m => Monad (t m) enabled by the
QuantifiedConstraints extension.
Monad morphisms solve the common problem of fixing monadic code after the
fact without modifying the original source code or type signatures. The
following sections illustrate various examples of transparently modifying
existing functions.
Generalizing base monads
Imagine that some library provided the following State code:
import Control.Monad.Trans.State
tick :: State Int ()
tick = modify (+1)
... but we would prefer to reuse tick within a larger
(StateT Int IO) block in order to mix in IO actions.
We could patch the original library to generalize tick's type signature:
tick :: (Monad m) => StateT Int m ()
... but we would prefer not to fork upstream code if possible. How could
we generalize tick's type without modifying the original code?
We can solve this if we realize that State is a type synonym for
StateT with an Identity base monad:
type State s = StateT s Identity
... which means that tick's true type is actually:
tick :: StateT Int Identity ()
Now all we need is a function that generalizes the Identity base monad
to be any monad:
import Data.Functor.Identity
generalize :: (Monad m) => Identity a -> m a
generalize m = return (runIdentity m)
... which we can hoist to change tick's base monad:
hoist :: (Monad m, MFunctor t) => (forall a . m a -> n a) -> t m b -> t n b
hoist generalize :: (Monad m, MFunctor t) => t Identity b -> t m b
hoist generalize tick :: (Monad m) => StateT Int m ()
import Control.Monad.Morph
import Control.Monad.Trans.Class
tock :: StateT Int IO ()
tock = do
hoist generalize tick :: (Monad m) => StateT Int m ()
lift $ putStrLn "Tock!" :: (MonadTrans t) => t IO ()
Example1 expression
>>> runStateT tock 0Tock!((), 1)
Monad morphisms
Notice that generalize is a monad morphism, and the following two proofs
show how generalize satisfies the monad morphism laws. You can refer to
these proofs as an example for how to prove a function obeys the monad
morphism laws:
generalize (return x)
-- Definition of 'return' for the Identity monad
= generalize (Identity x)
-- Definition of 'generalize'
= return (runIdentity (Identity x))
-- runIdentity (Identity x) = x
= return x
generalize $ do x <- m
f x
-- Definition of (>>=) for the Identity monad
= generalize (f (runIdentity m))
-- Definition of 'generalize'
= return (runIdentity (f (runIdentity m)))
-- Monad law: Left identity
= do x <- return (runIdentity m)
return (runIdentity (f x))
-- Definition of 'generalize' in reverse
= do x <- generalize m
generalize (f x)
Mixing diverse transformers
You can combine hoist and lift to insert arbitrary layers anywhere
within a monad transformer stack. This comes in handy when interleaving two
diverse stacks.
For example, we might want to combine the following save function:
import Control.Monad.Trans.Writer
-- i.e. :: StateT Int (WriterT [Int] Identity) ()
save :: StateT Int (Writer [Int]) ()
save = do
n <- get
lift $ tell [n]
We can mix the two by inserting a WriterT layer for tock and
generalizing save's base monad:
import Control.Monad
program :: StateT Int (WriterT [Int] IO) ()
program = replicateM_ 4 $ do
hoist lift tock
:: (MonadTrans t) => StateT Int (t IO) ()
hoist (hoist generalize) save
:: (Monad m) => StateT Int (WriterT [Int] m ) ()
Example1 expression
>>> execWriterT (runStateT program 0)Tock!Tock!Tock!Tock![1,2,3,4]
Embedding transformers
Suppose we decided to check all IOExceptions using a combination of
try and ErrorT: