Monad Transformers Tutorial
Think about code in IO that needs to be able to break out of a loop:
forM_ [1..maxRetries] $ \i -> do
response <- request i
when (satisfied response) break
Reminder about "when":
when False _ = return ()
when True a = a
So, how would you implement "break"?
Another example:
do
mc1 <- tryConnect "host1"
case mc1 of
Nothing -> return Nothing
Just c1 -> do
mc2 <- tryConnect "host2"
case mc2 of
Nothing -> return Nothing
Just c2 -> do
..
Clearly we want something like Maybe's (>>=)
here to catch the Nothing, but instead of Maybe values and functions we can combine with (>>=)
, we have IO (Maybe a)
values. So the "trick" is to implement the Maybe
monad again, this time, on IO (Maybe a)
values instead of Maybe a
values:
newtype MaybeIO a = MaybeIO {
runMaybeIO :: IO (Maybe a)
}
instance Monad MaybeIO where
return x = MaybeIO (return (Just x))
MaybeIO action >>= f = MaybeIO $ do
result <- action
case result of
Nothing -> return Nothing
Just x -> runMaybeIO (f x)
So now we can replace the above boilerplate code with:
result <- runMaybeIO $ do
c1 <- MaybeIO $ tryConnect "host1"
c2 <- MaybeIO $ tryConnect "host2"
..
Or if the tryConnect function wrapped its result in MaybeIO then we just have to use runMaybeIO there, and that's it. What happens if we now have some "print" in between?
result <- runMaybeIO $ do
c1 <- MaybeIO $ tryConnect "host1"
print "Hello"
c2 <- MaybeIO $ tryConnect "host2"
..
This wouldn't work, because the type of each statement in our do block is MaybeIO a
and not IO a
, so a (print "Hello") which is IO ()
cannot be put in there. This is where we want to "transform" an IO a
value to a MaybeIO a
value. All we have to do is convert the IO a
to an IO (Maybe a)
that doesn't "fail" our Maybe monad. So it just means putting a Just
in there:
transformIOtoMaybeIO :: IO a -> MaybeIO a
transformIOtoMaybeIO action = MaybeIO $ do
result <- action
return (Just result)
And now we can do:
result <- runMaybeIO $ do
c1 <- MaybeIO $ tryConnect "host1"
transformIOtoMaybeIO $ print "Hello"
c2 <- MaybeIO $ tryConnect "host2"
..
Now we can also break from the first example's loop!
break :: MaybeIO a
break = MaybeIO $ return Nothing
forM_ [1..maxRetries] $ \ i -> do
response <- transformIOtoMaybeIO $ request i
when (satisfied response) break
But, all of this code, while useful, is not in Haskell's spirit. Because MaybeIO really could work for any monad wrapping the Maybe, not just IO (Maybe a)
. The only IO operations all of the MaybeIO code performs is return and bind! So let's generalize our MaybeIO definition to all monads:
newtype MaybeT m a = MaybeT {
runMaybeT :: m (Maybe a)
}
instance Monad m => Monad (MaybeT m) where
return x = MaybeT (return (Just x))
MaybeT action >>= f = MaybeT $ do
result <- action
case result of
Nothing -> return Nothing
Just x -> runMaybeT (f x)
That was easy! I just replaced MaybeIO with MaybeT, IO with m, and added an "m" type parameter (with Monad constraint).
transformToMaybeT :: Monad m => m a -> MaybeT m a
transformToMaybeT action = MaybeT $ do
result <- action
return (Just result)
Again, really easy, just syntactic replacement of IO with a type parameter. Now, this "transformToMaybeT" operation is really common, because we don't just have MaybeT
, we also want EitherT
(when you want to break out of the loop or fail with a result!), ContT (lets you do really crazy things), ListT and many others. All of these have in common a lift
operation which is very much like "transformToMaybeT". So if we have:
transformToMaybeT :: Monad m => m a -> MaybeT m a
transformToEitherT :: Monad m => m a -> EitherT l m a
It seems we can capture this pattern with a class:
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
And now "t" is our MaybeT
(of kind (* -> *) -> * -> *
, i.e: two type parameters) and lift
is our transformToMaybeT, so:
instance MonadTrans MaybeT where
lift = transformToMaybeT
Why are they named Monad Transformers
? Note the kind signature of instances of the MonadTrans class: (* -> *) -> (* -> *)
. That is, every monad transformer type constructor takes a monad (kind * -> *
) as an argument, and returns a monad (also * -> *
) as a result. So all Monad Transformers are basically type-functions from monad types to different monad types which have additional traits.