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.