(On 2013.09.23, Aleksander Sumowski and I gave a talk on monads at the Developer Meetup, ThoughtWorks Pune. This post is a transcript of that talk.)
I have come across many people who are somewhat familiar with functional programming but have a hard time understanding the idea of monads. I believe monads are a simple concept, and the intent of this talk is to provide some intuition for them. And we promise not to use burritos in our explanation.
If you have not heard of monads before, this should still be approachable.
I will try to motivate the case for monads with some day-to-day code examples. The language used for the examples is Scala.
Example 1: Presence or absence of a value
You are given a string. You have to look up this string in a map named foo. The length of the value obtained is to be used as a key in another map, named bar, to get the final value.
This is how you might do it:
// snippet 1.1import java.{util => ju}import scala.collection.JavaConverters._val fooJ: ju.Map[String, String] = Map("a" -> "xoxox", "b" -> "xoxo").asJava
val barJ: ju.Map[Int, String] = Map(4 -> "P", 5 -> "Q").asJava
def calc(s: String): String = { val x = fooJ.get(s) val y = barJ.get(x.length) y}But wait. This code is incorrect. What if the key is missing in the first map itself? In that case the map will return null, leading to a NullPointerException in the next operation.
Clearly we need something that guards us against null, so we add an if check.
// snippet 1.2def calc(s: String): String = { val x = fooJ.get(s) val y = if (x != null) { barJ.get(x.length) } else { null } y}What was wrong with snippet 1.1 was that the two operations were sequenced wrongly. There were some additional effects necessary in between, and they were missing.
It would be nice if we could write code whose shape is like that of snippet 1.1 but which behaves like the code in snippet 1.2.
Example 2: Exceptions
You have to look up a couple of keys in a JSON object and then concatenate the obtained values with a space in between. Again, either or both keys might be absent, thus leading to an exception. In such an event, the exception should bubble up to the caller.
This is how you might do it:
// snippet 2.1import play.api.libs.json._
def calc(json: JsObject): String = { val name = (json \ "name").as[String] val address = (json \ "address").as[String] name + " " + address}This code works as expected. The necessary sequencing, or effects, here are provided by a first-class language feature called exceptions. Imagine a language without exceptions.
What might you do in such a language? Perhaps send a tuple of error code and result, or some equivalent structure.
Is it possible to write code whose shape and behaviour are like that of snippet 2.1, but which does not use first-class exceptions?
Example 3: Futures and promises
A future is an abstraction of a value that will be available at some point in time. This is a concurrency abstraction invented in the late 1970s but popularised by Java and JavaScript. Scala’s variation is a bit better.
Consider a simple, unrealistic task. We need to make two web service calls. Something mandates that the second call must be made after the first one is over. Then, after obtaining both results, you sum the lengths of the response bodies.
In a good old serial, blocking, non-future API, this might look like this:
// snippet 3.1def calc(s: String): Int = { val googleResponse = makeWebServiceCall("https://www.google.com/#q=" + s) val bingResponse = makeWebServiceCall("http://www.bing.com/search?q=" + s) googleResponse.body.length + bingResponse.body.length}Now let us rewrite this with an API that uses futures. How are we going to sequence operations in such an API? The popular solution seems to be callbacks. With such an approach the code might look like this:
// snippet 3.2import concurrent.{Promise, Future}import play.api.libs.ws.WS
def calc(s: String): Future[Int] = { val promise = Promise[Int]() val googleResponse = WS.url("https://www.google.com/#q=" + s).get() googleResponse onSuccess { case res1 => val bingResponse = WS.url("http://www.bing.com/search?q=" + s).get() bingResponse onSuccess { case res2 => promise.success(res1.body.length + res2.body.length) } } promise.future}The onSuccess here provides the necessary effects in between the operations.
This code is fairly straightforward, but it has the following problems:
- It exposes
Promise, an implementation detail of futures. - The
onSuccesspluspromisepattern gets repetitive fast and soon becomes an eyesore.
It would be nice if we could write code whose shape is like that of our serial code but which has future-like semantics.
Enter Type-Driven Development
Let me introduce you to a style of programming popular in statically typed functional languages: Type-Driven Development, or TyDD.
In TyDD, you encode as much semantic information as possible in your types and values. Of course there is a lot more to TyDD than that, but it is not the subject of this talk.
Let us revisit our examples and try applying TyDD to them.
Revisiting example 1
Scala has a data type named Option, which is a sum type that encodes presence or absence semantics. Languages with different type systems might use different encodings. It is roughly defined as:
// snippet 1.3sealed abstract class Option[+A]case class Some[+A](value: A) extends Option[A]case object None extends Option[Nothing]As you would expect, calling get on Scala’s Map returns an Option value. With Scala maps, our code might look like this:
// snippet 1.4val fooS: Map[String, String] = Map("a" -> "xoxox", "b" -> "xoxo")
val barS: Map[Int, String] = Map(4 -> "P", 5 -> "Q")
def calc(s: String): Option[String] = { fooS.get(s) match { case Some(x) => barS.get(x.length) case None => None }}One advantage of the TyDD approach should be immediately clear by now: the compiler will not allow you to treat Option[A] as A, and so you cannot inadvertently write incorrect code like snippet 1.1. You are forced to deal with optionality explicitly.
But all that sequencing with pattern matching looks yucky, and it will only get yuckier with every additional operation. Wouldn’t it be nice if the data type in question could take care of that sequencing itself?
As it happens, Option does have a method that takes care of this sequencing. The method is called flatMap, and when we use it, the code looks like this:
// snippet 1.5def calc(s: String): Option[String] = { fooS.get(s) flatMap { x => barS.get(x.length) flatMap { y => Some(y) } }}In the innermost block, you need to put the value back in Option. Some is the data constructor we use for that.
If you compare this code to snippet 1.1, you will notice that their shape is similar. The code is somewhat inverted along the vertical axis, but as you will soon see, there is a fix for that.
Revisiting example 2
Scala has a data type named Try that encodes success and failure semantics. It is roughly defined as:
// snippet 2.2sealed abstract class Try[+A]case class Success[+A](value: A) extends Try[A]case class Failure(throwable: Throwable) extends Try[Nothing]Let us imagine JsObject has a method named asTry[A] that returns Try[A]. With that, the code would look like this:
// snippet 2.3import play.api.libs.json._import util.{Try, Success, Failure}
def calc(json: JsObject): Try[String] = { (json \ "name").asTry[String] match { case Success(name) => (json \ "address").asTry[String] match { case Success(address) => Success(name + " " + address) case Failure(ex) => Failure(ex) } case Failure(ex) => Failure(ex) }}That is a fair amount of boilerplate.
As you might expect at this point, Try also has a method named flatMap that helps you do away with this boilerplate. This is how the code looks with flatMap:
// snippet 2.4import play.api.libs.json._import util.{Try, Success, Failure}
def calc(json: JsObject): Try[String] = { (json \ "name").asTry[String] flatMap { name => (json \ "address").asTry[String] flatMap { address => Success(name + " " + address) } }}In the innermost block, you need to put the value in Try, and we use the Success data constructor for that.
This code is very similar in shape to the code in snippet 2.1.
Revisiting example 3
Our results are already wrapped in Future, so we are already using a distinct type to encode the semantics.
Now Future also happens to have a flatMap method. Let us rewrite the code in snippet 3.2 using it.
// snippet 3.3import concurrent.{Promise, Future}import play.api.libs.ws.WS
def calc(s: String): Future[Int] = { WS.url("https://www.google.com/#q=" + s).get() flatMap { googleResponse => WS.url("http://www.bing.com/search?q=" + s).get() flatMap { bingResponse => Future.successful(googleResponse.body.length + bingResponse.body.length) } }}In the innermost block, we create a future that is already completed with our value.
Again, it is similar in shape to the code in snippet 3.1.
Monad comprehensions
As we have seen, flatMap allows our code to have a simple shape, with the details of sequencing abstracted away. Now monads are so common in functional programming that functional languages often provide a syntactic sugar on top of them which makes the inversion along the vertical axis go away. This sugar is called monad comprehensions. Scala calls them for-comprehensions. The similarity to for loops is superficial and should be ignored.
Here is how our code snippets might look once we start using this notation:
// snippet 1.6def calc(s: String): Option[String] = { for { x <- fooS.get(s) y <- barS.get(x.length) } yield y}
// snippet 2.5import play.api.libs.json._import util.{Try, Success, Failure}
def calc(json: JsObject): Try[String] = { for { name <- (json \ "name").asTry[String] address <- (json \ "address").asTry[String] } yield name + " " + address}
// snippet 3.4import concurrent.{Promise, Future}import play.api.libs.ws.WS
def calc(s: String): Future[Int] = { for { googleResponse <- WS.url("https://www.google.com/#q=" + s).get() bingResponse <- WS.url("http://www.bing.com/search?q=" + s).get() } yield googleResponse.body.length + bingResponse.body.length}These are even closer in appearance to snippets 1.1, 2.1, and 3.1 respectively.
Note that this is just syntactic sugar and compiles down, roughly, to the code we wrote before with flatMap.
So what is a monad?
A monad is essentially a two-method interface, which can be defined as follows:
// snippet 4.1trait Monad[M[_]] { def flatMap[A, B](x: M[A], f: A => M[B]): M[B] def point[A](x: A): M[A] def map[A, B](x: M[A], f: A => B): M[B] = flatMap(x, a => point(f(a)))}I am using the word interface here in its generic sense, not in the OO sense. A more specific term for this abstraction mechanism would be type class, and you can learn more about it here.
About the methods:
flatMap: we already talked about it.point: it is basically a method that allows you to put a value in the monad’s context in the innermost block.map: it is defined by default in terms offlatMapandpoint, and you can override it if a more performant implementation specific to the data type is possible.
As an example, here is a monad implementation for Option:
// snippet 4.2implicit object OptionMonad extends Monad[Option] { def flatMap[A, B](x: Option[A], f: A => Option[B]): Option[B] = x match { case Some(a) => f(a) case None => None }
def point[A](x: A): Option[A] = Some(x)
override def map[A, B](x: Option[A], f: A => B): Option[B] = x match { case Some(a) => Some(f(a)) case None => None }}The implementation of a Monad type class also needs to obey some laws, which we will not go into here.
Intuition for monads
You use the monad abstraction when:
- You need customised sequencing for operations, that is, you need additional effects in between computations and want them abstracted away.
- You need to abstract over the sequencing details, that is, write code that is parameterised over
M, some monad, and plug in a specific monad instance as necessary. This technique is used in Precog code to good effect.
Someone once described monads as something that lets you overload the semicolon. Since Scala does not use semicolons at the end of statements or expressions, the analogy is perhaps a little less vivid here, but the basic idea still holds.
Other monads
We have covered only three monads here, Option, Try, and Future, but there are many more:
Seqfor non-determinism.Eitherfor binary type disjunction, often used for error handling.Readerfor an implicit context passed around from which values can be read.Writerfor logging.Statefor stateful computations.STfor localised mutation.Undofor the ability to undo and redo operations.
Kleisli composition
Given two functions f: A => B and g: B => C, it is fairly trivial to compose them together. Here is one way to write such a function:
// snippet 5.1def compose[A, B, C](f: A => B, g: B => C): A => C = a => g(f(a))Now what happens when I have two effectful functions instead, say f: A => M[B] and g: B => M[C]? Composing these is not trivial, because the output type of f and the input type of g do not match. We need a new composition function with a signature like this:
// snippet 5.2def mcompose[A, B, C, M[_]](f: A => M[B], g: B => M[C]): A => M[C] = ???It is not possible to write this generically without knowing something about M. How the operations are composed depends on the structure of M, so we let M tell us how to wire them up:
// snippet 5.3def mcompose[A, B, C, M[_]](f: A => M[B], g: B => M[C])(implicit e: Monad[M]): A => M[C] = a => { val mb = f(a) e.flatMap(mb, { b => val mc = g(b) e.flatMap(mc, { c => e.point(c) }) }) }This kind of composition is called Kleisli composition and is often denoted with the symbol >=>.
Monads are not alone
I hope this talk helped you gain some intuition for what a monad is and when to use this abstraction.
As it happens, monads are not alone. There is a whole family of type classes that let you abstract over computational patterns. Some key examples include Functor, Applicative, MonadPlus, Arrow, and Alternative. If you find monads interesting or useful, I recommend exploring the rest of the family as well. The best medium for doing that is probably Haskell, and here is a good book for learning the language.
Some more notes
- The names
flatMapandpointare not standard, and different languages use different names. Some other names forflatMapare>>=,bind,m-bind, andSelectMany. Some other names forpointarepureandreturn. - Scala’s
Tryviolates certain monad laws and is therefore not quite a monad. However, those details are irrelevant to the basic nature of this talk. I could have usedEither, but its implementation would require touching on too many tangential ideas. - A question often asked is whether monads are possible or useful in dynamic languages, given how centred they are around types. Yes, they are possible in dynamic languages, but much of their utility is lost, and they require a somewhat different encoding. I intend to blog about this separately.
- Scala’s standard library does not have a
Monadtype class. How do its for-comprehensions work then? The answer is that the Scala compiler blindly translates them toflatMapandmapmethod calls on that data type. Yes,map, notpoint. This approach has its pros and cons. Nevertheless, aMonadtype class can be useful in many cases, and there are libraries that provide it. - If you want to understand monads more thoroughly, here is a very good tutorial. It is fairly long, but worth the time if the topic clicks for you.
- Monad comprehension sugar is neat, but it still leaves much to be desired. People have taken the idea further in many different directions and come up with better syntaxes, such as this one.