Lifting a monadic computation through a stack of monad transformers, part 2
In the previous post, we defined liftFunc'''
which lifts a computation in IO to MaybeT. If you do the same thing for StateT, you’ll get this function.
liftFunc''' :: ((forall a. StateT s IO a -> IO (a, s)) -> IO (b, s)) -> StateT s IO b
liftFunc''' f = let z s = f (flip runStateT s)
in StateT $ \s -> z s
By comparing liftFunc'''
for MaybeT and StateT, we’ll find that we can generalize liftFunc'''
by introducing a type class and a type synonym family.
{-# LANGUAGE TypeFamilies, RankNTypes #-}
import Control.Monad (liftM)
import Control.Monad.Trans (MonadTrans, lift)
import Control.Monad.Trans.Maybe (MaybeT(MaybeT), runMaybeT)
import Control.Monad.Trans.State (StateT(StateT), runStateT, put)
import Data.Tuple (swap)
class MonadTrans t => MonadTransFunc t where
type C t :: * -> *
liftFunc :: Monad m => ((forall a. t m a -> m (C t a)) -> m (C t b)) -> t m b
The type family C denotes a container that carries a monadic value in the base monad. It’s Maybe
for MaybeT
and (a, s)
for StateT s
. For example, we can define an instance of this class for MaybeT.
instance MonadTransFunc MaybeT where
type C MaybeT = Maybe
liftFunc f = MaybeT $ f runMaybeT
Although we want to define an instance for StateT like:
instance MonadTransFunc (StateT s) where
type C (StateT s) a = (a, s)
liftFunc f = StateT $ \s -> f $ flip runStateT s
we cannot do this because it applies two type parameters to C. So we have to swap the types in the tuple.
instance MonadTransFunc (StateT s) where
type C (StateT s) = (,) s
liftFunc f = StateT $ \s -> liftM swap $ f (liftM swap . flip runStateT s)
Now you can lift computations using liftFunc
.
testFunc0 :: IO a -> IO a
testFunc0 f = print "testFunc0" >> f
testFunc1 :: (Int -> IO a) -> IO a
testFunc1 f = print "testFunc1" >> f 1
maybeFunc0 :: MaybeT IO Int
maybeFunc0 = do lift $ print "maybeFunc0"
return 0
stateFunc1 :: Int -> StateT Int IO Int
stateFunc1 n = do lift $ print $ "stateFunc1: " ++ show n
put n
return n
maybe0 = runMaybeT go
where
go = liftFunc $ \run -> testFunc0 $ run $ maybeFunc0
state1 = runStateT go 0
where
go = liftFunc $ \run -> testFunc1 $ \x -> run $ stateFunc1 x
Note that you can lift a computation through two levels by applying liftFunc
twice.
maybeStateFunc1 :: Int -> MaybeT (StateT Int IO) Int
maybeStateFunc1 n = do lift $ lift $ print $ "maybeStateFunc1: " ++ show n
lift $ put n
return n
maybeState1 = runStateT (runMaybeT go) 0
where
go = liftFunc $ \runMaybe ->
liftFunc $ \runState ->
testFunc1 $ \x -> runState $ runMaybe $ maybeStateFunc1 x
But in most case, you may want to lift computations in a base monad directly through the stack of monad transformers as liftIO
and liftBase
do. How can we do that?