Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Providing typeclass instance for all MonadTrans instances

I've defined my own monad transformer:

data Config = Config { ... }

data State = State { ... }

newtype FooT m a = FooT {
      runFoo :: ReaderT Config (StateT State m) a
    } deriving (Functor, Monad, MonadReader Config, MonadState State)

And I've defined a MonadTrans instance for it.

instance MonadTrans FooT where
   lift = FooT . lift . lift

Now, I have various monads that I can't just have be derived for me by the compiler. I'll take MonadIO as the example. So I've defined my MonadIO instance as

instance MonadIO m => MonadIO (FooT m) where
    liftIO = lift . liftIO

However, I find that I'm doing a lot of lifting, for each Monad. Why could the author of each Monad typeclass (i.e. MonadIO, MonadCatchIO, MonadFoo) not define a general instance in terms of MonadTrans, instead of making me implement an instance for each new MonadTrans I come up with? a la

instance (MonadIO m, MonadTrans t, Monad (t m)) => MonadIO (t m) where
  liftIO = lift . liftIO

That requires UndecidableInstances to compile, and I'm not certain that it's correct (in fact, pretty sure it's incorrect), but serves to express my intent for now.

So, is this possible? If not, why not? Will it ever be?

like image 389
BluePeppers Avatar asked Nov 23 '22 01:11

BluePeppers


1 Answers

Let's say that I've come up with an alternative to MonadIO, called MyMonadIO. It's like MonadIO in every way, except for the name:

class Monad m => MyMonadIO m where
  myLiftIO :: IO a -> m a

Assuming your FooT type:

newtype FooT m a = FooT
  { runFoo :: ReaderT Config (StateT AppState m) a
  } deriving (Functor, Applicative, Monad, MonadReader Config, MonadState AppState)

It's possible to create an instance of MyMonadIO for ReaderT, StateT, and finally FooT. I've added extra type annotations to make it easier for the reader to figure out what's going on:

instance MyMonadIO m => MyMonadIO (ReaderT r m) where
  myLiftIO :: IO a -> ReaderT r m a
  myLiftIO = (lift :: m a -> ReaderT r m a) . (myLiftIO :: IO a -> m a)

instance MyMonadIO m => MyMonadIO (StateT s m) where
  myLiftIO :: IO a -> StateT s m a
  myLiftIO = (lift :: m a -> StateT s m a) . (myLiftIO :: IO a -> m a)

instance MyMonadIO m => MyMonadIO (FooT m) where
  myLiftIO :: IO a -> FooT m a
  myLiftIO = (lift :: m a -> FooT m a) . (myLiftIO :: IO a -> m a)

It's also possbile to use GeneralizedNewtypeDeriving to easily derive MyMonadIO for FooT (assuming there are already instances for ReaderT and StateT):

newtype FooT m a = FooT
  { runFoo :: ReaderT Config (StateT AppState m) a
  } deriving (Functor, Applicative, Monad, MyMonadIO, MonadReader Config, MonadState AppState)

If you look at the body of the myLiftIO function for the ReaderT, StateT, and FooT instances, they are exactly the same: lift . myLiftIO.

Here's a repeat of the question:

Why could the author of each Monad typeclass (i.e. MonadIO, MonadCatchIO, MonadFoo) not define a general instance in terms of MonadTrans, instead of making me implement an instance for each new MonadTrans I come up with?

For MyMonadIO, this general instance would be as follows:

instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n) where
  myLiftIO :: IO a -> t n a
  myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)

With this instance defined, you don't need a specific instance for ReaderT, StateT, or even FooT.

This requires UndecidableInstances. However, the problem with this is not undecidability, but that this instance overlaps some potentially valid instances of MyMonadIO.

For instance, imagine the following datatype:

newtype FreeIO f a = FreeIO (IO (Either a (f (FreeIO f a))))

instance Functor f => Functor (FreeIO f) where
  fmap :: (a -> b) -> FreeIO f a -> FreeIO f b
  fmap f (FreeIO io) = FreeIO $ do
    eitherA <- io
    pure $
      case eitherA of
        Left a -> Left $ f a
        Right fFreeIO -> Right $ fmap f <$> fFreeIO

instance Functor f => Applicative (FreeIO f) where
  pure :: a -> FreeIO f a
  pure a = FreeIO . pure $ Left a

  (<*>) :: FreeIO f (a -> b) -> FreeIO f a -> FreeIO f b
  (<*>) (FreeIO ioA2b) (FreeIO ioA) = FreeIO $ do
    eitherFa2b <- ioA2b
    eitherFa <- ioA
    pure $
      case (eitherFa2b, eitherFa) of
        (Left a2b, Left a) -> Left $ a2b a
        (Left a2b, Right fFreeIOa) -> Right $ fmap a2b <$> fFreeIOa
        (Right fFreeIOa2b, o) -> Right $ (<*> FreeIO (pure o)) <$> fFreeIOa2b

instance Functor f => Monad (FreeIO f) where
  (>>=) :: FreeIO f a -> (a -> FreeIO f b) -> FreeIO f b
  (>>=) (FreeIO ioA) mA2b = FreeIO $ do
    eitherFa <- ioA
    case eitherFa of
      Left a ->
        let (FreeIO ioB) = mA2b a
        in ioB
      Right fFreeIOa -> pure . Right $ fmap (>>= mA2b) fFreeIOa

You don't necessarily need to understand this FreeIO datatype (especially the Functor, Applicative, and Monad instances). It's enough just to know that this is a valid data type.

(If you're interested, this is just a free monad wrapped around IO.)

It's possible to write a MyMonadIO instance for FreeIO:

instance Functor f => MyMonadIO (FreeIO f) where
  myLiftIO :: IO a -> FreeIO f a
  myLiftIO ioA = FreeIO (Left <$> ioA)

We can even imagine writing a function using FreeIO:

tryMyLiftIOWithFreeIO :: Functor f => FreeIO f ()
tryMyLiftIOWithFreeIO = myLiftIO $ print "hello"

If you try to compile tryMyLiftIOWithFreeIO with both this instance (MyMonadIO (FreeIO f)) and the bad instance from above, you get the following error:

test-monad-trans.hs:103:25: error:
    • Overlapping instances for MyMonadIO (FreeIO f)
        arising from a use of ‘myLiftIO’
      Matching instances:
        instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n)
          -- Defined at test-monad-trans.hs:52:10
        instance Functor f => MyMonadIO (FreeIO f)
          -- Defined at test-monad-trans.hs:98:10
    • In the expression: myLiftIO $ print "hello"
      In an equation for ‘tryMyLiftIOWithFreeIO’:
          tryMyLiftIOWithFreeIO = myLiftIO $ print "hello"

Why does this happen?

Well, in instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n), what is the kind of t and n?

Since n is supposed to be a Monad, it's kind is * -> *. And since t is a monad transformer, it's kind is (* -> *) -> * -> *. t n is also supposed to be a Monad, so it's kind is also * -> *:

n :: * -> *
t :: (* -> *) -> * -> *
t n :: * -> *

Now, in instance Functor f => MyMonadIO (FreeIO f), what are the kinds of FreeIO and f?

f is supposed to be a Functor, so it's kind is * -> *. FreeIO's kind is (* -> *) -> * -> *. FreeIO f is a Monad, so it's kind is * -> *:

f :: * -> *
FreeIO :: (* -> *) -> * -> *
FreeIO f :: * -> *

Since the kinds are the same, you an see that instance Functor f => MyMonadIO (FreeIO f) overlaps with instance (Monad (t n), MyMonadIO n, MonadTrans t) => MyMonadIO (t n). GHC isn't sure which one to pick!

You can get around this by marking your instance FreeIO instance as OVERLAPPING:

instance {-# OVERLAPPING #-} Functor f => MyMonadIO (FreeIO f) where
  myLiftIO :: IO a -> FreeIO f a
  myLiftIO m = FreeIO (Left <$> m)

However, this is a treacherous route to go down. You can find out more about why overlapping can be bad from the GHC user guide.

This FreeIO example was created by Edward Kmett. You can find another clever example of an overlapping instance in this reddit post.


If you are planning on writing a monad typeclass (like MyMonadIO) and releasing it to Hackage, one option is to use the DefaultSignatures functionality. This makes it easier for users of your library to define instances.

Using DefaultSignatures, defining the MyMonadIO class would look like this:

class Monad m => MyMonadIO m where
  myLiftIO :: IO a -> m a
  default myLiftIO
    :: forall t n a.
       ( MyMonadIO n
       , MonadTrans t
       , m ~ t n
       )
    => IO a -> t n a
  myLiftIO = (lift :: n a -> t n a) . (myLiftIO :: IO a -> n a)

This says that there is a default implementation of myLiftIO for any t n, where n is an instance of MyMonadIO, and t is an instance of MonadTrans.

With this default siguature for myLiftIO, defining instances of MyMonadIO for ReaderT and StateT would look like this:

instance MyMonadIO m => MyMonadIO (ReaderT r m)
instance MyMonadIO m => MyMonadIO (StateT s m)

Very simple. You don't need to provide the function body of myLiftIO since it will use the default.

The only drawback of this is that it is not widely done. The DefaultSignatures machinery seems to be mainly used for generic programming, not monad typeclasses.

like image 164
illabout Avatar answered Dec 05 '22 04:12

illabout