Understanding monads

2017-01-12

Most experts agree that monads are the killer feature of category theory. However, it seems that beginners still have hard time grasping this concept. So, I decided to write up a post explaining what's wrong with most monad tutorials and explore their usage in Scala.

Monads in Java

When it comes to understanding monads, there are basically two ways to approach the problem: theoretical and practical. Many Internet resources try to explain monads using vocabulary from category theory, the branch of mathematics, which is regarded too abstract even by mathematicians themselves. Not surprisingly, these tutorials rarely achieve the desired effect and leave the readers even more confused than they were before. For example, one such tutorial says that two operations that monads need to support are:

  • the return operation that promotes regular values of type A to monads M[A]
  • the bind operation that takes a function A => M[B] that is used to transform underlying values

It you haven't heard about monads before, this is not particularly helpful.

It's better to approach the problem from an engineering standpoint. Engineers are interested in applying stuff to real-world problems, and this is where monads usually shine. However, in these kinds of tutorials, code examples are often written in Java or JavaScript.

The problem with using languages such as Java, JavaScript, Ruby (or 99% of other languages) is that they lack support for monadic structures on the syntax level. As a result, people easily grasp the idea, but fail to see its practical applications.

Think about it this way. Java 5 introduced a new syntactic construct known as the for-each loop:

Iterable<Integer> list = Lists.newArrayList(1, 2, 3);
for (Integer elem: list) {
    System.out.println(elem);
}

The code above works, because list has a type of Iterable. The official documentation says the following about this interface:

Implementing this interface allows an object to be the target of the "for-each loop" statement

The benefits of implementing this interface are obvious. If we make our own types the subtypes of the Iterable interface, we will be able to use them in for-each loops as well. On the other hand, creating interfaces called Monad doesn't give us much.

To illustrate this, let's take a look at the Optional type introduced by Java 8. This type has flatMap and map methods, which we can use to combine Optional values together:

Optional<String> maybeFirstName = Optional.of("Joe");
Optional<String> maybeLastName = Optional.of("Black");

Optional<String> maybeFullName = maybeFirstName.flatMap(firstName ->
    maybeLastName.map(lastName -> firstName + " " + lastName)
);

But that's about it. We can use constructions similar to the one shown above, but we cannot generalize them further due to the lack of support for monads on the language level.

for expressions

In Scala, the situation is very different. Scala introduces a new syntactic construct called for expression (also known as for comprehension or monad comprehension). The main purpose of for expressions is to provide a convenient and generic way to write flatMap/map combinations (support for iterating and filtering is secondary).

For example, using the Option class from the Scala standard library, we can combine two optional values using the same construct that we've seen before:

val maybeFullName = maybeFirstName.flatMap { firstName =>
  maybeLastName.map { lastName =>
    firstName + " " + lastName
  }
}

Alternatively, we can express the same thing using a for comprehension (I'm just copying this example directly from Modern Web Development with Scala):

val maybeFullName = for {
  firstName <- maybeFirstName
  lastName <- maybeLastName
} yield firstName + " " + lastName

There are many types in the Scala standard library that have map and flatMap methods. Not surprisingly, all of them are often used in for expressions. Want to combine several collections? You can use a for expression for that. Need to make several database requests? Just wrap everything in Try and use a for expression to combine results. Need to collect data from remote servers? Just grab corresponding Futures and put them in a for expression.

It's not surprising that these informally defined monads (or monad-like structures, i.e. everything with properly defined map and flatMap methods) are abundant in the standard Scala library and even in newbie code.

The Monad type class

Some people argue that it is the presence of higher-kinded types that makes monads useful in Scala. As I've just shown above, this is not actually correct. Defining and using monads (or monad-like structures) already makes sense if the language provides syntactic sugar for combining them.

However, if you want to properly define monads, having higher-kinded types supported by the language is essential. Fortunately, Scala doesn't have any problems in this department, and we can create a proper definition of the Monad trait (another example but this time copied from Mastering Advanced Scala):

trait Monad[F[_]] {
  def flatMap[A, B](fa: F[A])(f: (A) => F[B]): F[B]
  def pure[A](x: A): F[A]
}

The F[_] represents a type with one type argument. Obviously, the Option[A] type from the standard Scala library would perfectly fit this definition.

The two methods of the Monad trait directly correspond to the bind and return functions shown at the beginning. However, we also need the map method to be able to use our monads in for expressions. Luckily, there are several libraries in Scala that greatly simplify the process. When using Cats or ScalaZ, we are expected to implement only a couple of methods, and everything else (including map) will be derived automatically.

Note that pretty much every library defines monads using the type class pattern (see my previous post). This is actually quite important, because without implicit resolution provided by type classes, we would have to either write overly verbose code, or resort to inheritance.

Conclusion

Hopefully, my examples made it clear that monads are a very useful concept. At the same time, their usefulness actually depends on the language. If the language provides syntactic sugar for combining monad-like structures, supports higher-kinded types and type classes, monads will be very popular in this language. If it's not the case, monads will be extremely rare.