"What's a Monad?"
May. 6th, 2019 07:58 amThis one is mainly aimed at programmers, but I'm trying to teach a bit of programming, so it's not just for the FP geeks. (This is basically a bit of Functional Programming 101.)
In a comment on my earlier post, cellio asks what a Monad is. It's one of the most fraught questions in modern programming, but let's give it a stab. Bear with me -- this takes some explaining, and I am trying to avoid The Curse of Lady Monadgreen. This will be a practical description, rather than a precise formal one.
First, Functors
Let's consider several data types that we find in Scala. (And many other languages as well, often under other names, but I'm going to use Scala for my examples since it's what I know best. The concepts here are very general, but some languages express them better than others.)
On the one hand, take val o: Option[T]
. This means that o
is a box that may or may not hold a value of type T
: o
is either Some(t)
or None
. It's kind of like the idea of being able to be null
, but much more principled. (Many languages use this either instead of null
or provide it as an alternative.)
Next, let's look at val l: List[T]
. This means that l
is a box containing as many Ts as you want (possibly zero) with a specific order. (As opposed to Set[T]
, which has no intrinsic order.)
Finally, let's take val f: Future[T]
. This one doesn't exist in as many languages: it basically says that f
is a box that will eventually contain a T, assuming all goes well. (If all does not go well, it will eventually contain an error.) It's a good way to deal with asynchrony in programs, and can replace locks and callbacks and other such hell in many cases.
These types are all Functors. This means that they have a method called map()
, which means "do the following transformation with every element in me". So for example:
o.map(x => x * 2)
-- ifo
is, say,Some(5)
, this will returnSome(10)
. OTOH, ifo
isNone
, the result will still beNone
.l.map(x => x * 2)
-- ifl
isList(1, 2, 3)
, this will returnList(2, 4, 6)
.f.map(x => x * 2)
-- this returns anotherFuture
; iff
eventually contains a6
, the resultingFuture
will eventually contain12
. And iff
eventually returns an error, this resultingFuture
will contain that same error.
So the concept of a Functor[T] is that it is a box that contains "T-ness" in some fashion. You can transform the T's inside of it, getting another of the same kind of Functor with the transformed values.
Functors to Monads
All Monads are Functors, but with an additional kick: there is a sensible way to nest them. All of the above types are also Monads, and their Monad-ness can be expressed in for
comprehensions, like the following.
val maybeUser: Option[User] = ... val maybePhone: Option[PhoneNumber] = for { user <- maybeUser // Option[User] profile <- user.maybeProfile // Option[Profile] phone <- profile.maybePhone // Option[PhoneNumber] } yield phone
(I generally find it helpful to pronounce the <-
arrow as from "in". So the first line of that for comprehension reads roughly "for each user in maybeUser".)
What the above is saying is:
- I may have a
User
. - If so, put that
User
in a value nameduser
. - If that user has a value in
maybeProfile
, put that inprofile
. - If that profile has a value in
maybePhone
, put that inphone
. - If all that worked, return the phone number.
We are able to nest values or functions of type Option
, and get another Option
out at the end. If any of those steps returned None
, then we get None
out the end.
Next, let's do the same with List
:
val regions: List[Region] = ... val allCustomers: List[Customer] = for { region <- regions // List[Region] dealer <- region.dealers // List[Dealer] customer <- dealer.customers // List[Customer] } yield customer
This is saying:
- Start with a List of
Region
. - For each of those
Region
s, get all theDealer
s. - For each of those
Dealer
s, get all theCustomer
s. - Return all of those
Customer
s.
It's the same concept: there is a logical concept of nesting. More importantly, there is a specific way in which you nest: in the case of nesting List
s like this, the results will all be concatenated together: the Customers
for each Dealer
in order in the resulting List
, and all of the ones from Dealer
s in the same Region
together.
Finally, let's try Future
:
val userId: String = ... val profileFut: Future[Profile] = for { user <- userDb.loginUser(userId) // Future[User] prefs <- preferenceService.getPrefsFor(user) // Future[Prefs] avatar <- photoService.getAvatarFor(user) // Future[Photo] } yield new Profile(user, prefs, avatar)
Each step in this case is a function that returns a Future
. Putting it together:
- First, try logging in with the given
userId
, and get back a box that should eventually contain aUser
. - Once we have that
User
, get a box that will eventually contain theirPrefs
. - After we have fetched the
Prefs
, get a box that will eventually contain the user'sPhoto
. - When we've obtained all of that, assemble a
Profile
out of it.
Again, we are nesting these operations, and again, Future
imposes some details: these steps will be conducted in order, each one starting after the previous one has finished, and if any step returns an error, everything after that will fall through and return that error instead of trying to continue.
Note that each kind of Monad knows how to combine with itself. That doesn't mean that they can easily combine with each other -- there are sometimes ways to combine different Monads in sensible ways, but it's a bit ad-hoc: not all combinations make sense.
Conclusion
So those are the high concepts. A Functor is a sort of box that contains "T-ness", where you can transform the Ts inside of it. A Monad is a Functor that has specific rules for how it can be nested within itself.
And yes, the names are weird. They come from Category Theory, a branch of abstract mathematics that turns out to be really useful for serious programming. The video linked from my previous post includes the legendary line, "a monad is a monoid in the category of endofunctors": literally true and correct in Category Theory terms while being utterly useless for actually understanding anything.
Anyone who wants to dig into this a little should pick up the delightful book How to Bake Pi, a fairly slim, Kindle-friendly book that teaches Category Theory in terms of baking: each chapter starts with a recipe, and then shows how that recipe relates to a mathematical concept. It doesn't teach programming per se (and requires no background in either math or programming), but it provides the underpinnings that this stuff all grew out of. I found it invaluable for getting comfortable with this stuff.
For the actual programming concepts, including Functors, Monads and other such fun types, I currently recommend the documentation for the Cats library -- the Cats community has taken documentation really seriously, and the documentation of each type does a delightful job of exploring why that type works the way it does. It is aimed at programmers, though -- I wouldn't recommend coming into it completely cold.
Hopefully folks will find this at least a little helpful. Questions welcomed...