Skip to the content.

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?