Chris Stryczynski

Software Developer / Consultant

Hart article - A Gentle Introduction To Monad Transformers

A GitChapter Introduction to Monad Transformers

or, Values as Exceptions

A big thank you to Christoffer Stjernlöf https://two-wrongs.com/ who provided his work under (Creative Commons Share Alike license), as well as the other contributers, this article was originally posted at https://github.com/kqr/gists/blob/master/articles/gentle-introduction-monad-transformers.md.

This project is written with https://github.com/chrissound/GitChapter, this means you are able to clone down the project from https://github.com/chrissound/GentleIntroductionToMonadTransformers, and hack along from each section where you see the following:

Git From Commit: 
f47adfa0e7a25f13ed073599983c578773f6fc75

Git Until Commit: 
538140635ec8be16a7be0aae85c1916bced79fa4

The rewrite using GitChapter is partially completed. Hoping to finish this eventually!

Either Left or Right

Before we break into the mysterious world of monad transformers, I want to start with reviewing a much simpler concept, namely the Either data type. If you aren’t familiar with the Either type, you should probably not jump straight into monad transformers – they do require you to be somewhat comfortable with the most common monads.

With that out of the way:

Pretend we have a function that extracts the domain from an email address. Actually checking this properly is a rather complex topic which I will avoid, and instead I will assume an email address can only contain one @ symbol, and everything after it is the domain.

I’m going to work with Text values rather than Strings. This means if you don’t have the text library, you can either work with Strings instead, or cabal install text. If you have the Haskell platform, you have the text library.

We need to import Data.Text and set the OverloadedStrings pragma. The latter lets us write string literals (such as "Hello, world!") and have them become Text values automatically.

λ> :module +Data.Text
λ> :set -XOverloadedStrings

Now, figuring out how many @ symbols there are in an email address is fairly simple. We can see that

So if the split gives us just two elements back, we know the address contains just one @ symbol, and we also as a bonus know that the second element of the list is the domain we wanted. We can put this in a file.

This draws on our previous discoveries and is pretty self-explainatory. The function returns Right domain if the address is valid, otherwise Left InvalidEmail, a custom error type we use to make handling the errors easier later on. (Why this is called LoginError will be apparent soon.)

This function behaves as we expect it to.

To deal with the result of this function immediately, we have a couple of alternatives. The basic tool to deal with Either values is pattern matching, in other words,

Testing in the interpreter shows us that

Another way of dealing with Either values is by using the either function. either has the type signature

In other words, it “unpacks” the Either value and applies one of the two functions to get a c value back. In this program, we have an Either LoginError Text and we want just a Text back, which tells us what to print. So we can view the signature of either as

and writing printResult with the help of either yields a pretty neat function.

This function works the same way as the previous one, except with the pattern matching hidden inside the call to either.

Introducing Side-Effects

Git From Commit: 
538140635ec8be16a7be0aae85c1916bced79fa4

Git Until Commit: 
349c427a292d5e867c088bab22d030c7eec78fed

Now we’ll use the domain as some sort of “user token” – a value the user uses to prove they have authenticated. This means we need to ask the user for their email address and return the associated token.

So when getToken runs, it’ll get an email address from the user and return the domain of the email address.

λ> getToken
Enter email address:
test@example.com
Right "example.com"

and, importantly,

λ> getToken
Enter email address:
not.an.email.address
Left InvalidEmail

Now, let’s complete this with an authentication system. We’ll have two users who both have terrible passwords:

With an authentication system, we can also run into two new kinds of errors, so let’s change our LoginError data type to reflect that.

We also need to write the actual authentication function. Here we go…

checks that the email was processed without problems, finds the user in the collection of users, and if the passwords match, it returns the token to show the user is of authenticated.

If anything goes wrong, such as the passwords not matching, there not being a user with the entered domain, or the getToken function failing to process, then a Left value will be returned.

This function is not something we want to deal with. It’s big, it’s bulky, it has several layers of nesting… it’s not the Haskell we know and love.

Sure, it’s possible to rewrite it using function calls to either and maybe, but that wouldn’t help very much. The real reason the code is this ugly is that we’re trying to mix both Either and IO, and they don’t seem to blend well.

The core of the problem is that the IO monad is designed for dealing with IO actions, and it’s terrible at handling errors. On the other hand, the Either monad is great at handling errors, but it can’t do IO. So let’s explore what happens if you imagine a monad that is designed to both handle errors and IO actions.

Too good to be true? Read on and find out.

We Can Make Our Own Monads

We keep coming across the IO (Either e a) type, so maybe there is something special about that. What happens if we make a Haskell data type out of that combination?

What did we get just by doing this? Let’s see:

So already we have a way to go between our own type and the combination we used previously! That’s gotta be useful somehow.

Implementing Instances for Common Typeclasses

This section might be a little difficult if you’re new to the language and haven’t had a lot of exposure to the internals of how common typeclasses work. You don’t need to understand this section to continue reading the article, but I strongly suggest you put on your to-do list to learn enough to understand this section. It touches on many of the core components of what makes Haskell Haskell and not just another functional language.

If you want to read more about this kind of thing, [The Typeclassopedia][2] by Brent Yorgey is a comprehensive reference of the most common typeclasses used in Haskell code. Learn You a Haskell has a popular [introduction to the three typeclasses we use here][3]. Additionally, Adit provides us with [a humourous picture guide to the same three typeclasses][4].

But before we do anything else, let’s make EitherIO a functor, an applicative and a monad, starting with the functor, of course.

This may look a little silly initially, but it does make sense. First, we “unwrap” the EitherIO type to expose the raw IO (Either e a) value. Then we fmap over the inner a, by combining two fmaps. Then we wrap the new value up in EitherIO again, and return the wrapped value. If you are a more experienced Haskell user, you might prefer the following, equivalent, definition instead.

In a sense, that definition makes it more clear that you are just unwrapping, running a function on the inner value, and then wrapping it together again.

The two other instances are more of the same, really. Creating them is a mostly mechanical process of following the types and unwrapping and wrapping our custom type. I challenge the reader to come up with these instances on their own before looking below how I did it, because trying to figure these things out will improve your Haskell abilities in the long run.

In any case, explaining them gets boring, so I’ll just show you the instances as an experienced Haskell user might write them.

If your definitions look nothing like these, don’t worry. As long as your definitions give the correct results, they are just as good as mine. There are many ways to write these definitions, and none is better than the other as long as all are correct.