We have spent several weeks studying how the Monad
interface hides the plumbing involved with sequencing a bunch of actions
for many different types. However, we have not yet tried “stacking”
different types of actions. To demonstrate, we will consider a
simple programming task: (maybe) reading three integers from standard
input and summing them. After working through this example, maybe monad
transformers… will not seem so scary.
Read three Ints from user and sum them.
First read one Int…
readInt0 :: IO (Maybe Int)
readInt0 = do
s <- getLine
if s /= "" && all isDigit s then
pure $ pure $ read s
else
pure Nothing
Or:
readInt :: IO (Maybe Int)
readInt = do {- IO -}
s <- getLine
pure $ do {- Maybe -}
guard $ s /= "" && all isDigit s
pure $ read s
Then run it three times:
addThree :: IO (Maybe Int)
addThree = do
mi <- readInt
mj <- readInt
mk <- readInt
case (mi, mj, mk) of
(Just i, Just j, Just k) -> pure $ pure $ i + j + k
_ -> pure Nothing
Could use a do-block for the final expression.
addThree :: IO (Maybe Int)
addThree = do {- IO -}
mi <- readInt
mj <- readInt
mk <- readInt
pure $ do {- Maybe -}
i <- mi
j <- mj
k <- mk
pure $ i + j + k
But this always reads three lines, rather than failing as soon as one bad line is entered.
addThree :: IO (Maybe Int)
addThree = do {- IO -}
mi <- readInt
case mi of
Nothing -> pure Nothing
Just i -> do {- IO -}
mj <- readInt
case mj of
Nothing -> pure Nothing
Just j -> do {- IO -}
mk <- readInt
case mk of
Nothing -> pure Nothing
Just k -> pure $ pure $ i + j + k
Okay, let’s reduce the ugliness here. Define new versions of
bind and pure for the combination of
IO and Maybe, so that we can write:
addThree =
readInt `bindIOMaybe` \i ->
readInt `bindIOMaybe` \j ->
readInt `bindIOMaybe` \k ->
pureIOMaybe $ i + j + k
So:
pureIOMaybe :: a -> IO (Maybe a)
pureIOMaybe a =
pure $ Just a
bindIOMaybe :: IO (Maybe a) -> (a -> IO (Maybe b)) -> IO (Maybe b)
action `bindIOMaybe` f = do
ma <- action
case ma of
Nothing -> pure Nothing
Just a -> f a
These are “mashups” of pure and (>>=)
from the IO and Maybe instances. In fact,
these definitions work, not just for IO, but for any
Monad type m:
pureMonadPlusMaybe :: Monad m => a -> m (Maybe a)
pureMonadPlusMaybe a =
pure $ 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 -> pure Nothing
Just a -> f a
Exercise: Recall the characterization of
monads via join. Implement:
joinMonadPlusMaybe :: Monad m => m (Maybe (m (Maybe a))) -> m (Maybe a)
Now let’s plug this plumbing into the Monad interface,
to benefit from library and syntactic conveniences provided to
Monad.
The pattern we want is to “stack” actions of two Monad
types m and m':
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 an
For each such m', we will define a combination of
m and m', called MPrimeT m, that
forms a Monad by adding the functionality of
MPrime to m.
The Monad Transformer Library (MTL)
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.State
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Writer
defines the following:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
Let’s work through MaybeT, which is the combination we
need for our example:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
We’ll implement Monad first and take the free instances
of Functor and Applicative:
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
instance Monad m => Functor (MaybeT m) where {fmap f x = pure f <*> x}
instance Monad m => Applicative (MaybeT m) where {pure = return; (<*>) = ap}
The implementations of return and
(>>=) are just like pureMonadPlusMaybe
and bindMonadPlusMaybe, but with MaybeT and
runMaybeT sprinkled in.
Note: A minor ramification of taking the
free instance of Functor here is the Monad m
constraint, rather than just Functor m as strictly
necessary.
Let’s re-implement readInt and addThree to
be MaybeT IO Int actions rather than “raw”
IO (Maybe Int) actions.
We’ll need to “lift” m actions to MaybeT m
actions:
liftMaybeT :: Monad m => m a -> MaybeT m a
liftMaybeT ma = MaybeT $ fmap Just ma
And we’ll define a version of guard for
MaybeT m:
guardMaybeT :: Monad m => Bool -> MaybeT m ()
guardMaybeT True = MaybeT $ pure $ Just ()
guardMaybeT False = MaybeT $ pure Nothing
Now we can define the following (notice the difference compared to
the nested do blocks in the final version of
readInt; now there is just one, with the “stacking” taking
place inside)…
maybeReadInt :: MaybeT IO Int
maybeReadInt = do {- MaybeT IO -}
s <- liftMaybeT getLine
guardMaybeT $ s /= "" && all isDigit s
pure $ read s
… and run it thrice:
maybeAddThree :: MaybeT IO Int
maybeAddThree = do {- MaybeT IO -}
i <- maybeReadInt
j <- maybeReadInt
k <- maybeReadInt
pure $ i + j + k
> runMaybeT maybeReadInt
> runMaybeT maybeAddThree
This is like the final version of addThree, achieved via
the MaybeT plumbing — which allows Maybe to be
composed with any Monad, not just IO.
There are two minor improvements to make, which will clean up
maybeReadInt a bit more.
First, liftMaybeT is specific to the MaybeT
transformer. But, as we will see, we will want to lift functions to
different transformers. The following describes 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.
Pop Quiz: What kind of types does
MonadTrans classify?
Second, let’s make MaybeT m an Alternative,
which will unlock guard (rather than
guardMaybeT).
instance Monad m => Alternative (MaybeT m) where
empty :: MaybeT m a
empty = MaybeT $ pure Nothing
(<|>) :: MaybeT m a -> MaybeT m a -> MaybeT m a
mma <|> mmb = MaybeT $ do
ma <- runMaybeT mma
case ma of
Just a -> pure $ Just a
Nothing -> runMaybeT mmb
Note: Here’s an “overeager” variation of the above:
mma <|> mmb = MaybeT $ do
ma <- runMaybeT mma
mb <- runMaybeT mmb
pure $ ma <|> mb
And here’s a more elegant variation (courtesy Stuart Kurtz):
MaybeT mma <|> MaybeT mmb =
MaybeT $ (<|>) <$> mma <*> mmb
Now our final version of maybeReadInt:
maybeReadInt :: MaybeT IO Int
maybeReadInt = do
s <- lift getLine
guard $ s /= "" && all isDigit s
pure $ read s
Using TypeOperators,
enabled by default in GHC2021,
allows writing infix binary type applications. For example:
maybeReadInt :: IO `MaybeT` Int -- MaybeT IO Int
maybeAddThree :: IO `MaybeT` Int -- MaybeT IO Int