Monad Transformers
Monad hides the plumbing involved with sequencing a bunch of
operations. But they don't allow us to sequence different kinds
of operations.
Motivating Example
Read three Ints from user and sum them.
readInt :: IO (Maybe Int)
readInt = do
s <- getLine
if all isDigit s
then return $ Just (read s)
else return Nothing
addThree :: IO (Maybe Int)
addThree = do
mi <- readInt
mj <- readInt
mk <- readInt
case (mi, mj, mk) of
(Just i, Just j, Just k) -> return $ Just (i+j+k)
_ -> return Nothing
This replays the Maybe plumbing.
So, use a do-block for the final expression.
addThree :: IO (Maybe Int)
addThree = do
mi <- readInt
mj <- readInt
mk <- readInt
return $ do
i <- mi
j <- mj
k <- mk
return $ i + j + k
But this waits for all three lines before reporting error. Wrote it this way (initially with the tuple pattern-match) to save space. But would rather fail as soon as one bad line is entered.
addThree :: IO (Maybe Int)
addThree = do
mi <- readInt
case mi of
Nothing -> return Nothing
Just i -> do
mj <- readInt
case mj of
Nothing -> return Nothing
Just j -> do
mk <- readInt
case mk of
Nothing -> return Nothing
Just k -> return $ Just (i+j+k)
Okay, let's reduce the ugliness here.
Define new, specialized versions of bind and return to
combine Maybe and IO, so that we can write:
addThree =
readInt `bindIOMaybe` \i ->
readInt `bindIOMaybe` \j ->
readInt `bindIOMaybe` \k ->
returnIOMaybe $ i + j + k
So:
returnIOMaybe :: a -> IO (Maybe a)
returnIOMaybe a = return $ Just a
bindIOMaybe :: IO (Maybe a) -> (a -> IO (Maybe b)) -> IO (Maybe b)
action `bindIOMaybe` f = do
ma <- action
case ma of
Nothing -> return Nothing
Just a -> f a
These are "mashups" of return and (>>=) from the m and
Maybe instances.
In fact, the type signatures of these functions can be more
general:
returnMonadPlusMaybe :: Monad m => a -> m (Maybe a)
returnMonadPlusMaybe a = return $ Just a
bindMonadPlusMaybe :: Monad m => m (Maybe a) -> (a -> m (Maybe b)) -> m (Maybe b)
action `bindMonadPlusMaybe` f = do
ma <- action
case ma of
Nothing -> return Nothing
Just a -> f a
Much better than before, since we can hide the common
plumbing.
Now want to plug this functionality into Monad interface.
Monad Transformers
do-notation helps us with a single Monad type.
In the abstract example below, we are "in the m Monad".
do
x1 <- e0 -- e0 :: m (m' a1) ~= MPrimeT m a1
x2 <- e1 -- e1 :: m (m' a2) ~= MPrimeT m a2
x3 <- e2 -- e2 :: m (m' a3) ~= MPrimeT m a3
...
en -- en :: m (m' an) ~= MPrimeT m a
What we want is to define a combination of m and m'
(called MPrimeT m) that itself is a new Monad type.
Convention: The monad transformer FooT creates a new monad
from an existing Monad m (the "underlying" or "inner monad")
by adding the functionality of Foo (the "base monad") to m.
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.State
import Control.Monad.Trans.List
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
newtype ListT m a = ListT { runListT :: m [a] }
To make each of these useful, each of these transformers constructs types that are themselves monads.
MaybeT
The combination we want for our example:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
instance Monad m => Monad (MaybeT m) where
-- return :: a -> MaybeT m a
return = MaybeT . return . Just
-- (>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
MaybeT mma >>= f = MaybeT $ do
ma <- mma
case ma of
Nothing -> return Nothing
Just a -> runMaybeT $ f a
These are just like our returnMonadPlusMaybe and bindMonadPlusMaybe
implementations, except with MaybeT wrappers and runMaybeT unwrappers.
Having defined Monad first, we'll get Functor and Applicative for free:
instance Monad m => Functor (MaybeT m) where
fmap f x = pure f <*> x
instance Monad m => Applicative (MaybeT m) where
pure = return
(<*>) = ap
Let's try to re-implement readInt and addThree to be
MaybeT IO Int actions rather than IO (Maybe Int) actions.
First, we'll need to "lift" computations to MaybeT,
and sprinkle MaybeT wrappers and runMaybeT wrappers
elsewhere.
liftMaybeT :: (Monad m) => m a -> MaybeT m a
liftMaybeT ma = MaybeT (fmap Just ma)
maybeReadInt :: MaybeT IO Int
maybeReadInt = do
s <- liftMaybeT getLine
if all isDigit s
then return $ read s
else MaybeT $ return Nothing
maybeAddThree :: MaybeT IO Int
maybeAddThree = do
i <- maybeReadInt
j <- maybeReadInt
k <- maybeReadInt
return $ i+j+k
> runMaybeT maybeReadInt
> runMaybeT maybeAddThree
Much nicer than addThree from before.
And the plumbing allows Maybe to be composed with Monads
other than IO.
There are a couple more improvements to make.
First, maybeReadInt looks like readInt from before, but worse
because of the extra wrappers.
Would like to use MonadPlus to handle error cases, as with
Maybe. So, implement monoid structure.
instance (Monad m, Alternative m) => Alternative (MaybeT m) where
-- empty :: MaybeT m a
empty = MaybeT empty
-- (<|>) :: MaybeT m a -> MaybeT m a -> MaybeT m a
MaybeT mma <|> MaybeT mmb = MaybeT $ mma <|> mmb
(Note: This instance is defined with different class constraints in the library.)
maybeReadInt :: MaybeT IO Int
maybeReadInt = do
s <- liftMaybeT getLine
guard $ all isDigit s
return $ read s
Second, liftMaybeT is specific to the MaybeT transformer.
But, as we will see, we will want to lift functions to different
transformers.
Type class to describe a bunch of monad transformers:
class MonadTrans t where
lift :: Monad m => m a -> t m a
instance MonadTrans MaybeT where
-- lift :: Monad m => m a -> MaybeT m a
-- lift = liftMaybeT
-- lift ma = MaybeT (fmap Just ma)
lift = MaybeT . fmap Just
This will be just as easy for all other monad transformers.
Now life is good.
maybeReadInt :: MaybeT IO Int
maybeReadInt = do
s <- lift getLine
guard $ all isDigit s
return $ read s
maybeAddThree :: MaybeT IO Int
maybeAddThree = do
i <- maybeReadInt
j <- maybeReadInt
k <- maybeReadInt
return $ i+j+k