When you hear ‘Monad’, think ‘Chainable’

There comes a point in every Functional Programmer’s life where they feel the curse of the Monad has lifted and they must now explain Monads to their friends who just don’t get it. What follows is probably wrong and confusing, cause there is no escaping the curse. But here goes…

Suppose you have a system property that contains the name of another system property, like:

KEYNAME=FOO

And you want the value of FOO, like:

FOO=BAR

So there is a chain of operations. We need to first lookup the KEYNAME value and then use that to lookup another value.

In a non-functional world you may do something like:

String prop = null;
String keyname = system.getProperty("KEYNAME");
if (keyname != null) {
  prop = system.getProperty(keyname);
}

In the functional world we can instead use a type that represents the nullable value, usually called an Option, like:

val maybeKeyname: Option[String] = sys.props.get("KEYNAME")

Now we can’t use the maybeKeyname to lookup the second value because it might be None and props.get doesn’t take an Option:

sys.props.get(maybeKeyname) // this won't work

So we need to chain together two options. We can do this with Monads via a flatMap function:

val maybeProp: Option[String] = maybeKeyname.flatMap(keyname => sys.props.get(keyname))

Since flatMap takes a function we can also just do:

maybeKeyname.flatMap(sys.props.get)

But there is some syntactic sugar for Monads in Scala that we can use on anything that has the shape of a Monad (i.e. Monadic):

val maybeProp: Option[String] = for {
  keyname <- sys.props.get("keyname")
  prop <- sys.props.get(keyname)
} yield prop

The for comprehension makes Monad chaining look like a chain. So when you hear the word Monad just think chainable instead.

There are many different chainable types. Another is a Try for things that can fail.

For example, if we want to ask a user for two numbers and add them together, but handle number parsing failures we can do this:

import scala.io.StdIn
import scala.util.Try
 
for {
  num1 <- Try(StdIn.readLine("Number 1: ").toInt)
  num2 <- Try(StdIn.readLine("Number 2: ").toInt)
} yield num1 + num2

If you enter two numbers you get a Success but if either number is not an integer then the result will be a Failure.

Another Monadic type is Future which may hold a value later (i.e. async). You can chain futures together like Option and Try, for example:

import scala.concurrent.{Promise, Future}
import scala.concurrent.ExecutionContext.Implicits.global
 
// a Promise provides a place we write a value to later
val p1 = Promise[String]()
val p2 = Promise[String]()
 
val nameFuture: Future[String] = for {
  first <- p1.future
  last <- p2.future
} yield first + " " + last
 
// nameFuture does not yet have a value so lets write a value to the first Promise
p1.success("james")
 
// still no value for nameFuture because p1 and p2 are chained together and p2 doesn't have a value yet
p2.success("ward")
 
// ok, now nameFuture has a value - which you can print
nameFuture.foreach(println)

Monads just help us chain together operations on items in some form of container. In these examples the chains have been short (two operations) but could be much longer – making the value more apparent.