jducoeur: (Default)
[personal profile] jducoeur

This 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, [personal profile] 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) -- if o is, say, Some(5), this will return Some(10). OTOH, if o is None, the result will still be None.
  • l.map(x => x * 2) -- if l is List(1, 2, 3), this will return List(2, 4, 6).
  • f.map(x => x * 2) -- this returns another Future; if f eventually contains a 6, the resulting Future will eventually contain 12. And if f eventually returns an error, this resulting Future 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 named user.
  • If that user has a value in maybeProfile, put that in profile.
  • If that profile has a value in maybePhone, put that in phone.
  • 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 Regions, get all the Dealers.
  • For each of those Dealers, get all the Customers.
  • Return all of those Customers.

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 Lists 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 Dealers 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 a User.
  • Once we have that User, get a box that will eventually contain their Prefs.
  • After we have fetched the Prefs, get a box that will eventually contain the user's Photo.
  • 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...

(no subject)

Date: 2019-05-07 12:15 am (UTC)
fredrikegerman: (Default)
From: [personal profile] fredrikegerman
Congratulations, you have written the obligatory monad tutorial, which seems to be a rite of passage for everyone who ventures into Monadery. I was no exception...

(no subject)

Date: 2019-05-07 01:00 am (UTC)
metahacker: And then a miracle occurs... (You need to be more explicit in step 2, here!)  (miracle)
From: [personal profile] metahacker
I keep wanting article #2 in this series: "WHY are monads?"

If you need fodder for future articles. Like, since most things we write are monads, why is it important that we know they're monads? What do we commonly write that *isn't* a monad? what about that isn't a functor? When would you write those instead? What other useful categories are there, and what is their use?

(no subject)

Date: 2019-05-08 02:24 am (UTC)
cellio: (Default)
From: [personal profile] cellio
Thank you. The "sequential type" idea is new and I see the utility of that. (The goofy names are obfuscating factors for me. I should not need a graduate degree in math to understand programming concepts.) Functors, though, seem quite obvious; as I was reading I was thinking "yeah, so, this is just map()... what's the big deal?". Does that mean I missed something, or is that just background? (Though I grant that Future is a neat and novel concept that would make lots of ugly async code become more comprehensible.)

My first real programming language (as opposed to "used in university for classroom-sized projects") was LISP. That might have warped me. :-) I think of LISP as a functional language (I realize it can be used in other ways), but I don't think I'd ever heard the word "monad" until sometime in the last few years.

(no subject)

Date: 2019-07-21 06:14 am (UTC)
mindstalk: (Default)
From: [personal profile] mindstalk
> LISP was the first reasonably popular functional language (maybe the first functional language, period), long predating most of the rest.

Hell, it was the second high-level language, period, after Fortran but before COBOL.

Depends on how you define functional language, but I think from the beginning it let you pass around and manipulate functions -- "first class data type" -- which is pretty core. And had recursion and recursive data types, though it wasn't until the Scheme dialect that someone insisted tail calls be efficient.

(no subject)

Date: 2019-05-08 03:49 am (UTC)
cellio: (Default)
From: [personal profile] cellio
On further thought... as you've described it, isn't a monad ("sequential type") basically syntactic sugar around &&? That is, if you write A && B && C and A is false, you stop, or if A is true but B is false, you stop. I said "basically" because it's not strictly boolean with the None stuff and Futures are their own oddity, but from 20,000 feet, is this a fair comparison or have I missed something fundamental?
Edited Date: 2019-05-08 03:49 am (UTC)

(no subject)

Date: 2019-05-09 01:54 am (UTC)
cellio: (Default)
From: [personal profile] cellio
Oh, *dependency*. Yes, that makes sense. And also, I don't know that compilers *aren't* allowed to evaluate operands in whatever order they want; maybe A && B doesn't get evaluated in that order. (I guess one could find out by using expressions that have side effects, but... *shudder*.) I think you're right that "dependent" is a better description than "sequential", at least based on the vast understanding (cough) I've gained from reading this. :-)

(no subject)

Date: 2019-05-09 02:28 am (UTC)
cellio: (Default)
From: [personal profile] cellio
And this, in turn, is one of the beauties of the Futures approach -- it isn't nearly as susceptible to those bugs as traditional lock-based approaches.

But the complexity still has to go *somewhere*, right? It moves away from the programmer, it looks like, but that presumably increases the burden on the compiler or execution engine or whatever to get it right. *Somebody* has to unravel all the multi-threaded async crud, right? I mean, it's great that it's not me, but it's not free either, right?

Granted, concentrating the bug exposure in the compiler instead of distributing it among all programmers means that when a bug is found it's more likely to get fixed and benefit everyone. I'm not sure I'd want to be on the team that has to make that work, though. :-)

(no subject)

Date: 2019-07-21 06:17 am (UTC)
mindstalk: (Default)
From: [personal profile] mindstalk
> maybe A && B doesn't get evaluated in that order.

Depends on the language, but part of the point of C && is that it is order-dependent, as you said in your earlier post. If A is false, B isn't evaluated. Short-circuit evaluation.

(no subject)

Date: 2019-05-07 08:24 pm (UTC)
drwex: (WWFD)
From: [personal profile] drwex
This explanation raises a lot of questions that are likely nitpicky and would certainly make it longer. But I come out of this with effectively no more knowledge of "what is a monad" than I went in with.

I can't tell from this what's "mondad"-ness and what is "Scala"-ness. Like, it appears all your constructs have, effectively, error and null handling built in. Is that a property of Scala things or of monad things?

You have some capital "S" that make me want to ask whether that's a typo, a feature of the fonts you used or some attempt to call my attention to some notion of plurality.

You use "..." and I can't tell if that's a legal thing in your language or something you're using to avoid having to write out code that isn't relevant to the point you want to make.

You use "functor" which I see is linguistically similar to "function" but I'd have to look up what the difference is. Since all functors are monads (you say) I want to know that relationship, particularly since you use "function" once, in the very middle of everything.

I have a sneaking suspicion you could drop all the words about Futures which appear to be a neat thing you like and are using but aren't actually important to explaining what mondads are. If they have some importance in particular I failed to grasp it, sorry.

You talk about order in a somewhat incomplete way. I mean, all lists are "ordered" by definition but you seem to mean something else and maybe something more specific. And again I can't tell if this ordering behavior is a monad-think or a Scala-thing or just a neat thing you want to tell us about.

Profile

jducoeur: (Default)
jducoeur

June 2025

S M T W T F S
12 34567
891011121314
15161718192021
22232425262728
2930     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags