Prev: Combining monads the hard way TOC: Contents Next: Standard monad transformers

Monad transformers


Monad transformers are special variants of standard monads that facilitate the combining of monads. Their type constructors are parameterized over a monad type constructor, and they produce combined monadic types.

Transformer type constructors

Type constructors play a fundamental role in Haskell's monad support. Recall that Reader r a is the type of values of type a within a Reader monad with environment of type r. The type constructor Reader r is an instance of the Monad class, and the runReader::(r->a) function performs a computation in the Reader monad and returns the result of type a.

A transformer version of the Reader monad, called ReaderT, exists which adds a monad type constructor as an addition parameter. ReaderT r m a is the type of values of the combined monad in which Reader is the base monad and m is the inner monad. ReaderT r m is an instance of the monad class, and the runReaderT::(r -> m a) function performs a computation in the combined monad and returns a result of type m a.

Using the transformer versions of the monads, we can produce combined monads very simply. ReaderT r IO is a combined Reader+IO monad. We can also generate the non-transformer version of a monad from the transformer version by applying it to the Identity monad. So ReaderT r Identity is the same monad as Reader r.

If your code produces kind errors during compilation, it means that you are not using the type cosntructors properly. Make sure that you have supplied the correct number of parameters to the type constructors and that you have not left out any parenthesis in complex type expressions.

Lifting

When using combined monads created by the monad transformers, we avoid having to explicitly manage the inner monad types, resulting in clearer, simpler code. Instead of creating additional do-blocks within the computation to manipulate values in the inner monad type, we can use lifting operations to bring functions from the inner monad into the combined monad.

Recall the liftM family of functions which are used to lift non-monadic functions into a monad. Each monad transformer provides a lift function that is used to lift a monadic computation into a combined monad. Many transformers also provide a liftIO function, which is a version of lift that is optimized for lifting computations in the IO monad. To see this in action, we will continue to develop our previous example in the Continuation monad.

Code available in example21.hs
fun :: IO String
fun = (`runContT` return) $ do
        n   <- liftIO (readLn::IO Int)
        str <- callCC $ \exit1 -> do     -- define "exit1"
          when (n < 10) (exit1 (show n))
          let ns = map digitToInt (show (n `div` 2))
          n' <- callCC $ \exit2 -> do    -- define "exit2"
            when ((length ns) < 3) (exit2 (length ns))
            when ((length ns) < 5) $ do liftIO $ putStrLn "Enter a number:"
                                        x <- liftIO (readLn::IO Int)
                                        exit2 x
            when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns)
                                        exit1 (dropWhile (=='0') ns')  --escape 2 levels
            return $ sum ns
          return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
        return $ "Answer: " ++ str

Compare this function using ContT, the transformer version of Cont, with the original version to see how unobtrusive the changes become when using the monad transformer.

Nested monads from example 19 Monads combined with a transformer from example 21
fun = do n <- (readLn::IO Int)
         return $ (`runCont` id) $ do
           str <- callCC $ \exit1 -> do
             when (n < 10) (exit1 (show n))
             let ns = map digitToInt (show (n `div` 2))
             n' <- callCC $ \exit2 -> do
               when ((length ns) < 3) (exit2 (length ns))
               when ((length ns) < 5) (exit2 n)
               when ((length ns) < 7) $ do
                 let ns' = map intToDigit (reverse ns)
                 exit1 (dropWhile (=='0') ns')
               return $ sum ns
             return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
           return $ "Answer: " ++ str
fun = (`runContT` return) $ do
        n   <- liftIO (readLn::IO Int)
        str <- callCC $ \exit1 -> do
          when (n < 10) (exit1 (show n))
          let ns = map digitToInt (show (n `div` 2))
          n' <- callCC $ \exit2 -> do
            when ((length ns) < 3) (exit2 (length ns))
            when ((length ns) < 5) $ do
              liftIO $ putStrLn "Enter a number:"
              x <- liftIO (readLn::IO Int)
              exit2 x
            when ((length ns) < 7) $ do
              let ns' = map intToDigit (reverse ns)
              exit1 (dropWhile (=='0') ns')
            return $ sum ns
          return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
        return $ "Answer: " ++ str

The impact of adding the I/O in the middle of the computation is narrowly confined when using the monad transformer. Contrast this with the changes required to achieve the same result using a manually combined monad.


Prev: Combining monads the hard way TOC: Contents Next: Standard monad transformers