Making Sense of Multiple Monads

We’ve recently how the maybe monad has helped us avoid triangle of doom code patterns. Without it, we had to check each function call for success. However, the examples we looked at were all pure code examples. Consider this:

main :: IO
main = do
  maybeUserName <- readUserName
  case maybeUserName of
    Nothing -> print “Invalid user name!”
    Just (uName) -> do
      maybeEmail <- readEmail
      case maybeEmail of
        Nothing -> print “Invalid email!”
        Just (email) -> do
          maybePassword <- readPassword
          Case maybePassword of
            Nothing -> print “Invalid Password”
            Just password -> login uName email password

readUserName :: IO (Maybe String)
readUserName = do
  str <- getLIne
  if length str > 5
    then return $ Just str
    else return Nothing

readEmail :: IO (Maybe String)
...

readPassword :: IO (Maybe String)
...

login :: String -> String -> String -> IO ()
...

In this example, all our potentially problematic code takes place within the IO monad. How can we use the Maybe monad when we’re already in another monad?

Monad Transformers

Luckily, we can get the desired behavior by using monad transformers to combine monads. In this example, we’ll wrap the IO actions within a transformer called MaybeT.

A monad transformer is fundamentally a wrapper type. It is generally parameterized by another monadic type. You can then run actions from the inner monad, while adding your own customized behavior for combining actions in this new monad. The common transformers add T to the end of an existing monad. Here’s the definition of MaybeT:

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

instance (Monad m) => Monad (MaybeT m) where
    return = lift . return
    x >>= f = MaybeT $ do
        v <- runMaybeT x
        case v of
            Nothing -> return Nothing
            Just y  -> runMaybeT (f y)

So MaybeT itself is simply a newtype. It in turn contains a wrapper around a Maybe value. If the type m is a monad, we can also make a monad out of MaybeT.

Let’s consider our example. We want to use MaybeT to wrap the IO monad, so we can run IO actions. This means our new monad is MaybeT IO. Our three helper functions all return strings, so they each get the type MaybeT IO String. To convert the old IO code into the MaybeT monad, all we need to do is wrap the IO action in the MaybeT constructor.

readUserName :: MaybeT IO String
readUserName = MaybeT $ do
  str <- getLIne
  if length str > 5
    then return $ Just str
    else return Nothing

readEmail :: MaybeT IO String
...

readPassword :: MaybeT IO String
...

Now we can wrap all three of these calls into a single monadic action, and do a single pattern match to get the results. We’ll use the runMaybeT function to unwrap the Maybe value from the MaybeT:

main :: IO ()
main = do
  maybeCreds <- runMaybeT $ do
    usr <- readUserName
    email <- readEmail
    pass <- readPassword
    return (usr, email, pass)
  case maybeCreds of
    Nothing -> print "Couldn't login!"
    Just (u, e, p) -> login u e p

And this new code will have the proper short-circuiting behavior of the Maybe monad! If any of the read functions fail, our code will immediately return Nothing.

Adding More Layers

Here’s the best part about monad transformers. Since our newly created type is a monad itself, we can wrap it inside another transformer! Pretty much all common monads have transformer types in the same way the MaybeT is a transformer for the ordinary Maybe monad.

For a quick example, suppose we had an Env type containing some user information. We could wrap this environment in a Reader. However, we want to still have access to IO functionality, so we’ll use the ReaderT transformer. Then we can wrap the result in MaybeT transformer.

type Env = (Maybe String, Maybe String, Maybe String)

readUserName :: MaybeT (ReaderT Env IO) String
readUserName = MaybeT $ do
  (maybeOldUser, _, _) <- ask
  case maybeOldUser of
    Just str -> return str
    Nothing -> do
      -- lift allows normal IO functions from inside ReaderT Env IO!
      input <- lift getLine
      if length input > 5
        then return (Just input)
        else return Nothing

Notice we had to use lift to run the IO function getLine. In a monad transformer, the lift function allows you to run actions in the underlying monad. So using lift in the ReaderT Env IO action allows IO functions. Within a MaybeT (ReaderT Env IO) function, calling lift would allow you to run a Reader function. We don’t need this above since the bulk of the code lies in Reader actions wrapped by the MaybeT constructor.

To understand the concept of lifting, think of your monad layer as a stack. When you have a ReaderT Env IO action, imagine a Reader Env monad on top of the IO monad. An IO action exists on the bottom layer. So to run it from the upper layer, you need to lift it up. If your stack is more than two layers, you can lift multiple times. Calling lift twice from the MaybeT (ReaderT Env IO) monad will allow you to call IO functions.

It’s inconvenient to have to know how many times to call lift to get to a particular level of the chain. Thus helper functions are frequently used for this. Additionally, since monad transformers can run several layers deep, the types can get complicated, so it is typical to use type synonyms liberally.

type TripleMonad a = MaybeT (ReaderT Env IO) a

performReader :: ReaderT Env IO a -> TripleMonad a
performReader = lift

performIO :: IO a -> TripleMonad a
performIO = lift . lift

Typeclasses

As a similar idea, there are some typeclasses which allow you to make certain assumptions about the monad stack below. For instance, you often don’t care what the exact stack is, but you just need IO to exist somewhere on the stack. This is the purpose of the MonadIO typeclass:

class (Monad m) => MonadIO m where
  liftIO :: IO a -> m a

We can use this behavior to get a function to print even when we don’t know its exact monad:

debugFunc :: (MonadIO m) => String -> m a
debugFunc input = do
  liftIO $ print “Interpreting Input: “ ++ input
  …

One final note: You cannot, in general, wrap another monad with the IO monad using a transformer. You can, however, make the other monadic value the return type of an IO action.

func :: IO (Maybe String)
-- This type makes sense

func2 :: IO_T (ReaderT Env (Maybe)) string
-- This does not exist

Summary

Monad Transformers allow us to wrap monads within other monads. All of the basic built-in monads have transformer types. We name these types by adding T to the end of the name, like MaybeT. Monad transformers let us get useful behavior from all the different monads on our stack. The lift function allows us to run functions within monads further down the stack.

Monad transformers are extremely important when trying to write meaningful Haskell code. If you want to get started with Haskell, be sure to check out our free checklist for Haskell tools.

Want to practice some Haskell skills, but aren’t ready for monads? You can also take a look at our recursion workbook (it’s also free!). It has two chapters of content on recursion and higher order functions, as well as 10 practice problems.

Stay tuned, because next week we will complete our discussion of our abstract wrapper types (functors, applicatives, monads) by exploring the laws governing their behavior.

Previous
Previous

Obey the (Type) Laws!

Next
Next

The Monadic State of Mind