Home > Enterprise >  Implementing a liftIO-Style lift for Monad-Transformers in Haskell
Implementing a liftIO-Style lift for Monad-Transformers in Haskell

Time:10-13

I am currently learning to use Monad-Transformers in Haskell. I have composed a simple Monad-Transformer using StateT and ExceptT:

type DatabaseT m = StateT DatabaseState (ExceptT DatabaseError m)

There are already some functions working with DatabaseT like:

connectDB :: MonadIO m => DBMode -> DatabaseT m ()
isConnected :: Monad m => DatabaseT m Bool

To make lifting of such functions easier I want to define a type-class similar to MonadIO:

class Monad m => MonadDB m where
  liftDB :: Monad m' => DatabaseT m' a -> m a

Now the problematic part is making DatabaseT m an instance if MonadDB. Looking at the implementation of MonadIO this function should be the identity function:

instance Monad m => MonadDB (DatabaseT m) where
  liftDB = id

However this code does not compile:

Couldn't match type ‘m'’ with ‘m’
  ‘m'’ is a rigid type variable bound by
    the type signature for:
      liftDB :: forall (m' :: * -> *) a.
                Monad m' =>
                DatabaseT m' a -> DatabaseT m a
  ‘m’ is a rigid type variable bound by
    the instance declaration
  Expected type: DatabaseT m' a -> DatabaseT m a
    Actual type: DatabaseT m a -> DatabaseT m a
In the expression: id
  In an equation for ‘liftDB’: liftDB = id
  In the instance declaration for ‘MonadDB (DatabaseT m)’
Relevant bindings include
    liftDB :: DatabaseT m' a -> DatabaseT m a

It seems having a Monad-Transformer type-class for a Monad MyMonad, with liftMyMonad like functions, does not work when MyMonad is a Monad-Transformer itself.

Is this a valid approach to make lifting of DatabaseT m actions more generic or is it fundamentally flawed?

If so, how can I lift DatabaseT m actions from anywhere on top of a DatabaseT stack?

CodePudding user response:

The end state you ask for probably isn't the end state you want. Namely this is not great for most needs:

class Monad m => MonadDB m where
  liftDB :: Monad m' => DatabaseT m' a -> m a

The typical solution is to not write such an instance at all and instead write:

class Monad m => MonadDB m where
  connectDB :: DBMode -> m ()
  isConnected :: m Bool

This allows you to:

  • Use the functions in monadic contexts where DatabaseT is a component
  • Use solutions like MonadMock for testing
  • Not break abstraction - avoid using lift in the consumer code at all.
  • Related