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.