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

Source Files

results matching ""

    No results matching ""