ianmorgan.github.io

Blog - Explaining a Monad

Follow me on GitHub

Explaining a monad in 10 steps

I thought i’d try and explain a monad, partly for the selfish reason that it would force me understand them better myself. Example code is in Scala.

Step 1 - Categories are Containers

Monads come from Category Theory. Categories are just containers with a set of common behaviours that should be true for all types and values. Of course formally proving they work for anything may be difficult, but in the real world we can normally rely upon intuition and common sense to decide what is good category. A list is a classic example.

scala > val names = List("John", "Paul", "Ringo", "George")
names: List[java.lang.String] = List(John, Paul, Ringo, George)

scala> names.size
res0: Int = 4

Step 2 - Changing data in our container

Our containers are values, and so are their content. That is they are immutable and so they can’t be changed directly. And so to manipulate them we need to create a new container based on the old ones. Converting from one value to another is a pure function:

f: X -> Y

The container needs to provide its clients a way to pass in the function. In Scala this is by convention called the map method.

scala> names.map(_.toUpperCase)
res1: List[java.lang.String] = List(JOHN, PAUL, RINGO, GEORGE)

Step 3 - Composing small changes together

Pure functions will compose in a repeatable way, so that a more complex function can be built.

f: X -> Y
g: Y -> Z

is the same as:

h: X -> Z

Which can also be drawn something like:

Commutative diagram for morphism" by User:Cepheus - Own work, based on en:Image:MorphismComposition-01.png. Licensed under Public domain via Wikimedia Commons.

The same is true of the map method. They may be chained together.

scala> names.map(_.toUpperCase).map(_.reverse)
res2: List[String] = List(NHOJ, LUAP, OGNIR, EGROEG)

Step 4 - What happens if my function creates a new type of container (aka category)

So far we have used functions that just return simple types. But what if the function returned something more complex?

scala> val names = List("John L","Paul M")
names.map(_.split(" "))
res3: List[Array[java.lang.String]] = List(Array(John, L), Array(Paul, M)

So this returns a list of lists, which is a new type of container

To make it simpler, it can be ‘flattened’ out into one list

scala> names.map(_.split(" ")).flatten
res4: List[java.lang.String] = List(John, L, Paul, M)

This is such a common pattern it can be combined into a single method, flatMap

scala> names.flatMap(_.split(" "))
res5: List[java.lang.String] = List(John, L, Paul, M)

Step 5: That was a monad, so whats special about it?

So, flatMap is fact a monad. The name is a little arbitrary. In Haskell its called bind, which is the name used in category theory. The clever thing is that its joined two containers into. In this example its two lists and they are simply concatenated (flattened) together. Strictly speaking only flatMap must be defined as map is really just a special case of flatMap with a single value. But for ease of use both are usually provided.</p>

There are two really useful things here.

Firstly, cardinality (does the function return zero, one or multiple values) becomes much less important which make designs more flexible. A similarity is joins in SQL, the syntax stays the same regardless as to how many joins exist. In fact, the traditional imperative style of a variable for a single value, a null for something that doesn’t exist and a container that must be manually unpacked for multiple values is often unnecessarily cumbersome

Secondly many common problems can be modelled as a Category (aka container) with common behaviours. These include (using Scala’s names) Option (holder for value that may be null) , a Try (holder for a computation that may have resulted in an exception) and Future (holder for result of an asynchronous computation)

Step 6: Using an Option monad

In Scala (and Java 8) the Option can be used to wrap a result that may be null, for example it could not be found in the database. Option has methods to read the value and so on. It also provides map and flatMap. So in a suitably trivial example, we might have a function that returns user names from a database:

scala> def findName (id:Integer) : Option[String] = if (id == 1) Some("John") else None
res6: findName: (id: Integer)Option[String]

and another to get an address if we know the name

scala> def findAddress (name:String) : Option[String] = if (name.equals("John")) Some("Liverpool") else None
res7: findAddress: (name: String)Option[String]

These can now be safely chained together without worrying about null pointers.

scala> findName(1).map(findAddress(_))
res8: Option[Option[String]] = Some(Some(Liverpool))
scala> findName(2).map(findAddress(_))
res9: Option[Option[String]] = None

And flatMap will get rid the Option of Option complexity. The logic here is simple. If the container (i.e. the instance the flatMap is called on) is None, then it must be a None. If not, the function is applied and its result returned.</p>

scala> findName(1).flatMap(findAddress(_))
res8: Option[String] = Some(Liverpool)
scala> findName(2).flatMap(findAddress(_))
res9: Option[String] = None

Of course in this little example the effect seems a bit contrived but hopefully the benefit in more complex real world scenario is clear. In Scala the Option type has a number of other useful tricks, one of which is to be able to think of itself as a collection with either one value or empty. Here is good guide.