Ok, I know what you’re thinking, but no, this is not another tutorial about monads. During the entirety of this article, you’re not going to find a single definition of monads whatsoever. If you’re looking for definitions of monads I guarantee you can find plenty of them by using your search engine of choice.
Instead, my goal here is to show how some languages can leverage monads in order to allow for more composable, concise and cleaner code.
So, without further ado, let’s get started!
Every time I try to understand what a given technology does or how it works, I like to begin by understanding what problems it’s trying to solve. So, let’s start by looking at a problem that can be solved by using monads.
First, take a look at the following code which does a lot of null checks:
case class RegularPerson(name: String, reportsTo: RegularPerson) {
}
object RegularPerson {
def getSupervisorsSupervisorName(employee: RegularPerson): String = {
if (employee != null) {
val supervisor = employee.reportsTo
if (supervisor != null) {
val supervisorsSupervisor = supervisor.reportsTo
if (supervisorsSupervisor != null) {
supervisorsSupervisor.name
} else {
null
}
} else {
null
}
} else {
null
}
}
}
For those not familiar with Scala, the RegularPerson case class defines a person with two attributes: a name, and a supervisor that person reports to (which in turn is a RegularPerson itself). That means one could create a chain of nested persons going on forever.
The getSupervisorsSupervisorName method receives a person as a parameter and tries to retrieve the person’s supervisor’s supervisor’s name. I could have stopped at the person’s supervisor but I think going down one more level helps to make the problem more obvious.
Before you say something, I know there are better ways of implementing such a function but the point I’m trying to make here is that this scenario (null checking) is a very common one and it can be solved by applying monads if your language is “monad-aware”.
So, let’s take a look at one possible solution:
case class Person(name: String, reportsTo: MyOption[Person]) {
}
object Person {
def getSupervisorsSupervisorName(maybeEmployee: MyOption[Person]): MyOption[String] = {
for {
employee <- maybeEmployee
supervisor <- employee.reportsTo
supervisorSupervisor <- supervisor.reportsTo
} yield supervisorSupervisor.name
}
}
Much cleaner, right?
The first thing we should notice in the code above is that we’ve encapsulated the Person object inside a container called MyOption. We could’ve used Scala’s native Option class here but I wanted to show how we can create our own monads if we wanted/needed to.
Now, let’s focus on the for comprehension piece of the code.
for {
employee <- maybeEmployee
supervisor <- employee.reportsTo
supervisorSupervisor <- supervisor.reportsTo
} yield supervisorSupervisor.name
What this code does is to extract the value (Person) out of the MyOption container (three times) and then return a result encapsulated with the same container (MyOption[String]). If you are not familiar with Scala’s inners, the following code snippet shows what the compiler would translate that code to:
maybeEmployee
.flatMap(employee => employee.reportsTo)
.flatMap(supervisor => supervisor.reportsTo)
.map(supervisorSupervisor => supervisorSupervisor.name)
That means in order for this code to work our container class (MyOption) must implement those two methods (map and flatMap). Let’s take a look at those methods’ signatures:
sealed trait MyOption[+A] {
def map[B](f: A => B): MyOption[B]
def flatMap[B](f: A => MyOption[B]): MyOption[B]
}
We can see that the map function receives a function as a parameter and returns an instance of our container (MyOption). If you’re not familiar with Scala’s traits, you can think of it as an interface like the ones in C# and Java.
Now let’s take a look at one of the concrete implementations of the map and flatMap functions:
case class MySome[A](value: A) extends MyOption[A] {
def map[B](f: A => B): MyOption[B] = MySome[B](f(value))
def flatMap[B](f: A => MyOption[B]): MyOption[B] = f(value)
}
The first thing we must notice is that the MySome class extends the MyOption trait. Again, if you’re not familiar with Scala, that would be like a class implementing an interface or an abstract class.
By taking a closer look at the map function signature, we can notice that the function we pass to it receives an instance of the same type that the MyOption container encapsulates (in this case Person) and returns any type we want (in our case that would be a String). Finally, the map function encapsulates the result of the function we’re passing back into the MyOption container.
For our example, that means we need to pass to the map function a function which receives a Person instance and returns a String. In our example above (the one with the map and flatMap calls) we pass the following function (lambda):
supervisorSupervisor => supervisorSupervisor.name
Whose signature is:
Person => String
Which matches the signature the map function expects.
Let’s pause for a while to think about what that means… the map function takes a function that applies a transformation to a value of a given type and returns a value of a (possible) different type. After applying the transformation, the map function encapsulates the result of the transformation into the container type the map function is defined for.
Since the result type of the map function is guaranteed to be of the same type of the container we’ve defined map and flatMap for, we can be sure we can call map or flatMap in the result value as well. This is a similar pattern to the Builder design pattern where one can chain method calls in order to construct an object.
There’s one problem though. If we chain multiple calls to map passing a function that returns our container (which is exactly what we’re doing in the for comprehension) we’ll end up with nested instances of our container as the result (think of a Russian doll). Here’s an example:
val value = MySome("a").map(x => MySome(x)).map(x => MySome(x))
> value: MyOption[MySome[MySome[String]]] = MySome(MySome(MySome(a)))
The reason this behavior becomes a problem is that it doesn’t allow us to chain (or compose) function calls. At least not without adding a lot of extra work in order to access the contents of the nested containers. Finally, each statement of the for comprehension block expects a function that returns the same container type (not necessarily with the same content type though). That means we need a function with a different signature to be able to compose our function calls nicely.
Ok, so far, so good. Now let’s take a look at the implementation of the flatMap function:
def flatMap[B](f: A => MyOption[B]): MyOption[B] = f(value)
By looking at its signature we can see that it expects as a parameter a function that receives an instance of the same type our container encapsulates and returns another (any) type encapsulated in our container (MyOption).
For our example, that means we need to pass to the flatMap function a function which receives a Person instance and returns a MyOption[Person]. In our example above (the one with the map and flatMap calls) we pass the following functions (lambdas):
employee => employee.reportsTo
and
supervisor => supervisor.reportsTo
Which both have the following signature:
Person -> Person
Which matches the signature the flatMap function expects.
Ok, so let’s pause again and think about the flatMap function behavior for a while… Let f be the function we pass to flatMap as a parameter. Differently from the map function, flatMap doesn’t require us to encapsulate the result of the call to f into the container type since f must already return the container type as its result. Let’s see an example of a series of nested calls to flatMap where f returns the container type and notice how it differs from the same example using map:
val value = MySome("a") .flatMap(x => MySome(x)) .flatMap(x => MySome(x))> value: MyOption[String] = MySome(a)
Pretty cool, huh? This looks a lot more composable. Actually, it’s just what the for comprehension expects. This shouldn’t be a surprise though since we’ve already seen that the for comprehension is just syntax sugar for a series of calls to flatMap with yield being translated to a call to map.
Ok, so you might now be asking: what does all this mean and how does it relate to monads?
Let’s start by recapping what we’ve seen so far:
- Scala has a for comprehension operator that under the hoods, translate to a series of flatMap and map calls.
- The for comprehension operator (as well as the flatMap function) allows us to compose function calls that otherwise would require extra boilerplate code in order to process the results of those function calls and pass them along to other functions.
- We saw that by encapsulating our values into containers and using the flatMap function defined for those containers it’s possible to decouple our functions and make them composable.
Well, it turns out that our MyOption class is a monad. And so are Lists, Futures(?), Options and many other types implemented by the Scala standard library. One can think of monad as an interface with two methods (bind and unit) that must be implemented by its concrete types. By implementing the flatMap function (bind) and the apply function (unit) we’re implementing our own monadic type. For those not familiar with Scala, the apply function is like a constructor we get for free when defining a case class.
You might be wondering, what about the map function though? Why did we have to implement it if it’s not part of the monad “interface”? The answer is because it’s required by the for comprehension statement. Remember that the for comprehension statement is translated to a series of flatMap calls with the yield statement being translated to a map call. Without the map function we wouldn’t be able to call the yield statement. There are other functions that are also not part of the monad “interface” but could be implemented in order to get more functionality out of the for comprehension statement, like, for example, filtering.
C# is another language which also leverages monads. You can achieve a similar behavior to the one provided by Scala’s for comprehension by using LINQ’s from statement. If you’re curious, you can take a look at the source code for this article on this Github repo. It contains examples in both Scala and C#.
That’s it for now! What I wanted you to take from this article is that some languages can take advantage of the behavior provided by monads even if you don’t know what monads are. Whenever you use a for comprehension to iterate over a List or access the content of an Option you’re leveraging the power of monads and making your code cleaner and simpler and less susceptible to errors.
However, I’d be lying if I said that’s all. There’s a lot more to monads though. Beyond implementing those two functions, a monad needs to comply with a set of laws or rules. Also, monads allow us among other things, to make our functions pure by isolating side effects, etc, but I’ll leave that to articles that focus on those aspects since I promised you this is was not going to be a monads tutorial. If you’re curious about what the monads’ laws are you can take a look at this article or Google for one of the many monads tutorials available.
Let me know your thoughts on this article! You can reach me on Twitter or Linkedin. Thanks for reading and till next time!