Monad Transformers
In the past few parts of this series, we've learned a lot of new monads. In part 3 we saw how common things like Maybe
and IO
can be monads. Then in part 4 and part 5 we learned about the Reader
, Writer
, and State
monads. With all these monads under out belt, you might be wondering how we can combine them. The answer, as we'll discover in this part, is with monad transformers!
As you understand monads more and more, you'll unlock many new Haskell abilities. But you still need some idea of the Haskell libraries that will let you exercise these! Be sure to download our Production Checklist to learn about some of these libraries!
If you want to see the code from this part "in action" you should head over to the Github Repository for this series! Take a look at this module for the complete code seen in this article. If you want to follow along and fill in some TODOs along the way, you can took a look at this incomplete version!
Motivating Example
Earlier in this series, we saw how the maybe monad 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:
main1 :: IO ()
main1 = 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
putStrLn "Please enter your username!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readEmail :: IO (Maybe String)
readEmail = do
putStrLn "Please enter your email!"
str <- getLine
if '@' `elem` str && '.' `elem` str
then return $ Just str
else return Nothing
readPassword :: IO (Maybe String)
readPassword = do
putStrLn "Please enter your Password!"
str <- getLine
if length str < 8 || null (filter isUpper str) || null (filter isLower str)
then return Nothing
else return $ Just str
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
putStrLn "Please enter your Username!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readEmail' :: MaybeT IO String
readEmail' = MaybeT $ do
putStrLn "Please enter your Email!"
str <- getLine
if length str > 5
then return $ Just str
else return Nothing
readPassword' :: MaybeT IO String
readPassword' = MaybeT $ do
putStrLn "Please enter your Password!"
str <- getLine
if length str < 8 || null (filter isUpper str) || null (filter isLower str)
then return Nothing
else return $ Just str
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
:
main2 :: IO ()
main2 = 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 $ Just str
Nothing -> do
-- lift allows normal IO functions from inside ReaderT Env IO!
lift $ putStrLn "Please enter your Username!"
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. This behavior is encompassed by the MonadTrans
class:
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
So using lift
in the ReaderT Env IO
action allows IO
functions. Using the type template from the class, we can substitute Reader Env
for t
, and IO
for m
.
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 ()
debugFunc input = liftIO $ putStrLn ("Successfully produced input: " ++ input)
So even though this function doesn't explicitly live in MaybeT IO
, we can write a version of our main
function to use it.
main3 :: IO ()
main3 = do
maybeCreds <- runMaybeT $ do
usr <- readUserName'
debugFunc usr
email <- readEmail'
debugFunc email
pass <- readPassword'
debugFunc pass
return (usr, email, pass)
case maybeCreds of
Nothing -> print "Couldn't login!"
Just (u, e, p) -> login u e p
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
Now that you know how to combine your monads together, you're almost done with understanding the key concepts of monads! You could probably go out now and start writing some pretty complex code! But to truly master monads, you should know how to make your own, and there's one final concept that you should understand for that. This is the idea of type "laws". Each of the structures we've gone over in this series has a series of laws associated with it. And for your instances of these classes to make sense, they should follow the laws! Check out part 7 to make sure you know what's going on!
Now that you can write some pretty complex code, you need to know some of the libraries that will help you use it! Download our Production Checklist for a summary of some awesome libraries to help you apply your skills! Haskell has many tools for tasks like building web APIs and accessing databases. Now that you know all about monads, you can use these quite easily!