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 Int
s 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 Monad
s
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