Intro to monad transformers in Haskell
NOTE: this tutorial assumes you are already familiar with monads. If not, I would recommend learning about monads first.
Why monad transformers?
Let’s say we have functions with the following signatures:
Imagine we are building an app and this is an API we are given to access specific user’s data.
These functions take user’s id and go into the world to perform an IO action - e.g. a
network request, or read from the disk. Also, an IO action can fail if e.g.
there was no network connection available - that is why Maybe
is in the return type.
With that, we want to implement the following function:
This function tries to fetch all the data for the specific user (name, surname and age) and if
any piece of this data couldn’t be obtained it will declare itself as failed by returning
Nothing
within IO monad.
This is how we would go about implementing fetchUsersData
:
And this would work! The only not so nice thing is that we have this “staircasing” typical when
nesting multiple Maybe
s. We would normally solve this by running things within a Maybe
monad,
but this time we are in IO
monad so we can’t do that!
To generalize the problem, it occurs when we have one monad within
another (Maybe
within IO
here) and would like to access “powers” of the inner monad to make
our code nicer.
This is a common case when using monads in Haskell and exactly what monad transformers are set to
solve - making it possible to user “powers” of one monad within another monad. In this case, we
would like to extend IO
’s capabilities with Maybe
’s power to exit the computation early.
What is a monad transformer?
Here is what the documentation says:
A monad transformer makes a new monad out of an existing monad, such that computations of the old monad may be embedded in the new one.
And then there is a typeclass MonadTrans
which every monad transfomer has to implement, which means
monad transformer is actually a type. Monad transformer is a monad as well.
Here is how it looks like in our case, where we want to extend IO
with Maybe
capabilities:
MaybeT m a
is a monad transformer version of Maybe a
. There is only that extra m
which stands
for an “inner” monad that will be expanded with Maybe
capabilities.
Regarding the named field of MaybeT
(runMaybeT
), here’s another bit from the documentation:
Each monad transformer also comes with an operation runXXXT
to unwrap the transformer, exposing a computation of the inner monad.
If we wrapped IO
within MaybeT
and then couldn’t get it back again that would be a problem,
since the function we are implementing needs to return IO (Maybe (String, String, Int))
.
From this we can generalize and conclude that a monad transformer has:
- one additional type parameter (compared to its monad counterpart) - an inner monad that is being expanded with capabilities of the outer monad
runXXXT
function/field which returns the original, “inner” monad
What does it mean “monads don’t compose”?
If you have been learning about monad transformers, odds are you came across this statement. I did too, and often it was the first thing discussed when talking about monad transformers. But I couldn’t understand what does it mean to “compose” monads nor why it is a problem if that cannot be done.
As you also probably know and as we saw in the example with Maybe
, each monad has its monad
transformer counterpart (Maybe
and MaybeT
, Either
and EitherT
, …). Each of these monad
transformer counterparts had to be manually and separately implemented by somebody.
That is exactly what the statement in question (monad composition) challenges - why do we have to
do so much work, do we really need to implement xxxT
version of each monad? It would be awesome
if we could somehow automate this.
And that is where composing monads would come in handy. Monad composition is creating a new type which is parametrized by (any) two monads and is then also itself a monad. It would look like this:
This is the general case of the example above, where m1
and m2
were IO
and Maybe
,
respectively (one monad wrapped in another).
Now the main question here is can we implement the following:
If we could, that would mean MonadComposition m1 m2
is also a monad. Meaning that we solved
the general case of composing any two monads! If that was true, we wouldn’t need to bother
with implementing MaybeT
, EitherT
, ReaderT
separately - MonadComposition
would cover all
of that for us automatically!
But as you reckon, that is not the case. It is not possible to make MonadComposition m1 m2
an
instance of Monad
typeclass. It can be proved and it is not trivial. From the intuitive
perspective, we can understand that we need more information, the general case is not covering it.
If the “outer” monad is Maybe
, we need to implement what MaybeT
will do in terms of Nothing
and Just x
, which means we need the specifics.
So that is it! Now you know what “monads don’t compose” means and why it is important.
MaybeT
We used it as an example in the introduction, but let’s now officially take look at it.
From the docs:
The MaybeT
monad transformer extends a monad with the ability to exit the computation without returning a value.
A sequence of actions produces a value only if all the actions in the sequence do. If one exits, the rest of the sequence is skipped and the composite action exits.
Here is the type definition:
m
being an arbitrary monad composed with Maybe
monad. Let’s now see it in action with our example from the beginning:
We can see from the types this works. We don’t end on the left hand side of <-
anymore with Maybe a
which we then have to
unpack (leading to the staircasing we saw before), but we get a
directly.
But what actually happens behind the curtains? Let’s see how MaybeT
implements Monad
typeclass:
We can see that actually here MaybeT
does the heavy lifting for us, what we previously did
manually (staircasing example) - it operates within m
(IO
in the example) and gets to Maybe a
and then does that “manual” check whether it is Nothing
or not.
Lifting
We said monad transformers are all about combining the powers of two or more monads. With our
MaybeT IO a
example we’ve shown how we can get Maybe
’s power and make sure that the whole computation is
short-circuited to Nothing
if some sub-computation failed. But what if we wanted to do
some IO stuff, e.g. print Fetching data...
?
This is why we have lift
, which is the only function of MonadTrans
typeclass:
Argument monad is the “inner” monad (IO
in our example) and constructed monad is the actual
monad transformer (MaybeT
in our case).
So this is the function that allows us to “lift” the inner monad’s computation into the monad transformer’s realm, hence the name. It allows us to do this:
lift
here does exactly what it says in its signature, takes IO ()
and lifts it into MaybeT IO ()
so we can
call this printing action within MaybeT
monad.
It is also maybe interesting to see how MaybeT
implements lift
:
In the case of printing from above, IO ()
would come in, liftM Just
would produce IO (Just ())
and then MaybeT
data constructor would create an instance of MaybeT IO ()
type.
Phew, we just went through our first monad transformer! Let’s now take a look at another one - ExceptT
.
ExceptT
ExceptT
is a monad transformer version of Either
and is very similar to MaybeT
, just as Either
is similar to Maybe
.
The only difference is that in the case of the failure the exception that is thrown actually contains a value (additional error data) rather than just Nothing
.
Here is the type definition:
So let’s just quickly go through the analogous example - let’s assume we are given following API functions:
And we want to implement:
If we try do to it without ExceptT
monad transformer, we’ll again bump into the staircasing issue:
Now let’s try with ExceptT
:
Nice! Let’s also take a look at how ExceptT
implements Monad
:
And that’s it! This is almost exactly the same as MaybeT
, the only difference being the error message that Either
brings with itself.
ReaderT
As we know, Reader
monad is useful