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
And this would work! The only not so nice thing is that we have this “staircasing” typical when
Maybes. We would normally solve this by running things within a
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
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 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
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
Regarding the data constructor and field of
MaybeT, 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
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) - a monad that is being expanded
runXXXTfunction/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 (
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
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
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
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
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.
We used it as an example in the introduction, but let’s now officially take look at it.
From the docs:
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
But what actually happens behind the curtains? Let’s see how
We can see that actually here
MaybeT does the heavy lifting for us, what we previously did
manually (staircasing example) - it operates within
IO in the example) and gets to
and then checks does that “manual” check whether it is
Nothing or not.
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
Nothing if some sub-computation failed. But what if we wanted to do
some IO stuff, e.g. print
This is why we have
lift, which is the only function of
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 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
It is also maybe interesting to see how
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 on -