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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With