An Easy to Understand Monad Guide

Aaron Meltzer
12 min readAug 19, 2020

--

Motivation

The motivation for creating this guide is, from what I’ve seen, the widespread sense that Monads are difficult to understand and hard to use. The guide is structured in a way that makes Monads as easy to understand for the widest possible audience. That said, this is a living document. If any part of the guide is still confusing, I’ll do my best to make edits to better explain the concept.

Monads

Monads are a powerful construct that can make your code safer and easier to understand. Still, a proliferation of Monad tutorials that try to build up to them from other Functional Programming concepts makes what is otherwise a simple concept difficult to understand.

The goal of this guide is to explain Monads in a way that is easy for programmers with minimal knowledge of Functional Programming to understand. In fact, the only prerequisite Functional Programming concept for this guide is the pattern of passing functions as parameters to functions. To that end, the tutorial will start from a concrete example, and slowly build up to the abstract definition of a Monad.

This guide will also generally make use of Java syntax so that it can be understood by a wider audience than using less well-known languages that may be able to express Monads better.

Short History

Despite entering the general zeitgeist only recently, Monads were introduced to programming around 1990. Monads were initially used in niche Functional Programming languages. Since the popularization of Functional Programming over the last couple years, Monads have gained wider recognition as useful constructs. In fact Java Optionals are a kind of Monad, and were introduced in Java 8 at least in part to aid in avoiding NullPointerExceptions.

Basic Everyday Use-Case

The simple real-life example we will be starting with is the utilization of Java’s Optional, and how it can help improve your code. While it is widely known that Optionals aid in avoiding NullPointerExceptions, if you use Optionals properly as a Monad, the safety and utility of Optionals multiply many fold.

To start with, let us consider a simplified museum management API endpoint. You give the API endpoint a name of a city, and it returns the most popular museum’s curator in the city. But what happens if the user submits the name of a city that does not have any museums? Signaling that there are no curators in the city is a valid answer, so you simply return a null.

Several months later, you are using your API as part of another project. You are confident that it works, but it has been awhile, and you forget that a null can be returned. You do not check if the return value can be null and get hit by a null pointer exception. Not much you can do, right? A null return value is a valid answer, so you just need to put null checks around the API.

Enhancing the Type System

But what if we could encode the possibility of a “none” scenario into your type system so that you did not have to remember to null check every object? In a work setting, what if you could signal to your coworkers that a “none” scenario is possible, without needing formal documentation, or a separate conversation? This is where Optional comes in.

Instead of simply returning a String representing the name of the museum, you could return Optional<String>. Even if the only improvement is replacing the null check with an Optional.isPresent, we are still gaining the following: we are now explicitly modeling in code which values can be null, and which values cannot be null.

This allows us to signal directly in code where null values are possible. Good code practice unloads more of the work to the computer and less from the programmer so that the programmer is freed up to solve higher level problems. Using Optional on every nullable value removes the need for programmers to remember, or in a team context, learn, every value that can be null.

In this sense, an Optional type is, at least in part, providing the programmer context about the variable above and beyond what the variable can store. In this case, context means that there is additional logic surrounding the value above and beyond the usual logic implied by the underlying type.

In effect, a Monad is a way to model, handle, propagate, and work with context surrounding a value. This is particularly valuable when the context surrounding a value implies differential handling depending on what the context is. In our example, the context is whether the value is null. When the value is null, then there needs to be some form of error handling or default value, and when it is not null, computation can continue as normal.

Context and Monads

Returning to the Optional example, the Optional specifically represents the context that a value may not exist. Now what can we do with the Optional? Moving back to our original example, oftentimes people new to Optionals will use Optional.isPresent to check if the value is there, and then Optional.get to retrieve the underlying value in order to use or manipulate it. Under the hood, Optional.isPresent returns true if the underlying value is not null, and false if it is null. Optional.get returns the underlying value, unless it is null, in which case it throws an exception. The combination of Optional.isPresent and then Optional.get guarantees Optional.get will not throw an exception.

If you have several functions executed in a sequence where each one takes in the underlying value in an Optional, and returns an Optional, then Optional.isPresent and Optional.get will need to repeatedly be called. One of the issues with using Optional.get is that it removes the context that the value might not be there

Such a usage would look like:

That’s pretty verbose, and there are multiple points where you need to remember to the same ifPresent check over and over again, but at least we know what can or cannot be null just by the type system. This is not much of an improvement, especially considering modern programming languages like Kotlin have a built in way to flag if a variable can be null or not, but we’re just getting started with Monads.

What if we could use the context, aka the Optional, to do that work for us? Optional itself is just a class, which means the Optional class itself could contain functions that take care of the additional logic associated with the Optional context.

Moreover, the type of the underlying data does not matter. Any type of data an Optional can contain, can be null. Therefore, we can encode null checks inside the Optional with a single function and reuse that solution for any type of data. Rather than peppering null checks all over our code base, we have solved null checks once, and can reuse that solution throughout our code base.

How can we get the Optional object to do this work for us? Optional has a function called Optional.flatMap, which looks like:

As you can see, if TypeInsideContext is the Optional generic, Optional.flatMap takes a function, where the function takes in an object of type TypeInsideContext, and returns an Optional of type NewTypeInsideContext where NewTypeInsideContext may or may not be the same type as TypeInsideContext. Inside Optional.flatMap, it does a null check on the variable in the Optional, and if it is not null, then the value is passed to the function passed to flatMap, and if it is null, an empty Optional is returned.

You can then take that Optional returned, and use the returned flatMap to chain the next function, and so on and so forth. Inside, Optional is doing the null check and continuing the chain of functions if the value is not null. If at any point the value is null, then the result of the chain of functions is always an empty Optional. This way you can concentrate on coding your business logic in the form of functions and let Optional handle error cases.

Rewriting the previous example with chaining:

Moreover, because the last flatMap returns Optional<Curator>, and not Curator, the type system forces us to find a way to return Curator, or else it will not compile. In effect, the type system itself forces us to provide code for the two scenarios: one in which the underlying value is present, and one where it is not. The function to do this is orElse, which provides a default if the underlying value is not present, but otherwise provides the underlying value, and looks like:

So how does this relate to Monads? The flatMap function, where it takes a function that takes a type T and returns an Optional<U>, and does null checks, is an Optional specific implementation of one of the Monad functions. Specifically, it is the implementation of the generic Monad flatMap function.

More General Generics

To fully describe what flatMap is, we need to discuss how generics work. Java has some generics. For example, the Optional class itself has the generic TypeInsideContext. This allows us to define Optional once, without needing to know what exact type the Optional will contain. All we know is that it is some Object, and that’s all we need to know.

While this could also be done with inheritance, if we defined Optional as taking an Object, then if we retrieved the underlying value in the Optional, we would have no way to knowing what sub-type of Object it is. With generics, we can maintain that information.

While Optional can currently take a type without a generic, such as String, or a type with a generic, such as List<String>, what if we needed to restrict Optional to taking any type that had a generic? We could restrict it to Collection, but what if we wanted Optional to be able to take a non-Collection type with an generic? We could define a generic that takes a single generic, such as A<U>, but this is not currently allowed in Java. As a side-note, the official term for this is Higher Order Generics, but that would be its own article.

The current flatMap definition, without more general generics, is restricted to:

where flatMap were an abstract function on the abstract Monad class. You could define several Monad implementations of the flatMap function, but they would all be restricted to operating on Optionals.

What if instead, we could represent that “Optional” can be any type that is parameterized by a single type? For this article, any type parameterized by a single type will be represented as something like A<U> where A is any type that takes in one type. As with other generics, all instances of the symbol A<U> will be bound to the exact same type.

What is the benefit of this? First, it lets us express that the return value of a Monad’s flatMap should be the same kind of Monad but may or may not be parameterized by a different type. After all, two different generics can be bound to the same type.

This guarantee of getting back the same Monad enables the chaining of flatMaps we saw with Optionals. Secondly, it helps us enforce that every Monad implement flatMap, and thereby gain all the benefits of flatMap without necessarily knowing the particular implementation details of the flatMap.

We have now introduced and seen how to use the flatMap function, which is the primary function associated with Monads. There is one more bit of notation required to do so because Java cannot represent any type that takes in a type. The representation will look like:

You might have also noticed the addition of a constructor. This is not just a nicety to satisfy Java’s Object-Oriented nature. In fact, for a type to be considered a Monad, it must implement a function with effectively the same type signature as a constructor.

An Optional specific implementation would look like:

By now, we have already seen why these two functions are so useful. The constructor allows us to lift up any type to the Optional type. The flatMap function allows us to execute functions without having to be worried about NullPointerExceptions. There are more ways Monads can be useful, and a flatMap more generally can be said to enforce a particular constraint around the lifted up type.

And that’s basically it! While Monads are touted as hard to understand, whether a type qualifies as a Monad is as simple as if it implements those two functions.

Monad Laws

To be complete, we do still have to go over the Monad Laws. The Monad Laws are basically rules for implementing Monad functions that should be followed. They allow for Monad behavior to be restricted in such a way that we can confidently predict how Monad functions will behave in certain scenarios. The laws are:

Left Identity

This law ensures that wrapping a value in a Monad and passing a function that takes the underlying value to flat map is the same as passing the underlying value directly to the function.

Right Identity

Where myMonadicValue is of type M<A>, i.e it is an instance of a Monad:

This law ensures that if we use flatMap to apply a monad to its own constructor, the result is just the same monad.

Associativity

This law ensures that it doesn’t matter if we turn a value into a monadic value, and then apply two functions, or if we apply the first function (which itself returns the result in a Monad) and then use flatMap to apply the second function.

Another Example

While we now have all the tools needed to understand all the criteria for what constitutes a Monad, I have found it helpful to run through at least a couple examples in order to gain a better understanding of when Monads will be useful.

In this scenario, you’re writing code for a financial instrument trading system, and this system is for a large hedge fund. One hypothetical regulation from the government is mandating that every step in the sequence of steps to conduct a trade must be reported to the government in a particular format, and with particular stats as applicable. This requirement is particularly well suited for a Monad because:

The underlying functionality does not need to change.

  • How this fits with Monads — Since the additional functionality is independent of the underlying functionality, flatMap provides us a convenient way to wrap the underlying functionality while maintaining it separately.
  • Also, utilizing wrapping the functionality in a Monad enables us to guarantee the functionality will be executed no matter where the data flows in the system. If all the executions can be centralized in one place, then that would be a good solution. This is not always possible, and a Monad approach would work in every case.
  • Finally, the requirement that the underlying functionality not change is guaranteed in the Left Identity rule of a Monad.

The order in which the trading operations do not matter. Regardless of the order, the operation should be reported.

  • How this fits with Monads — Monad associativity guarantees that the results will be the same regardless of the order in which the Monads are executed.

Finally the Right Identity simply provides a nice guarantee that passing a monad constructor to flatMap will just result in the same Monad back. This makes Monads more resilient to errors.

Monad Implementation:

This is also an example of a commonly mentioned use-case for Monads — when you need to express a side-effect, but due to the danger of side-effects, want to keep the side-effect isolated and clearly marked. In this case, the side-effect is the IO action of submitting data to an external service.

The last benefit you might notice from this implementation is that the requirement that this particular operation be reported to the government is now embedded in the type system. Functions that accept something of type FinancialRegulationMonad are known, without the need for any external documentation, to be regulated trade operations. Functions that do not accept this type are known to be operations that regulations do not require to be reported. This is a similar benefit that Optional gives in that Optional indicates directly in the type system whether or not a value can be null. In the Optional case, a Runtime error is reduced to a compile time error, but in this case, the new type FinancialRegulationMonad reduces a logical error to a compile time error.

More general Monads may themselves take a generic, like how Optional does. In this particular case we do not need to, but it is not hard to see how FinancialRegulationMonad spanning multiple types could utilize a generic to avoid re-declaring the same basic class over and over again.

The enhancement of the type system to reduce many kinds of errors to compile time errors is a benefit Monads provide regardless of what the Monad does.

Final Notes

People familiar with the more usual Functional Programming Monad definition will note that the type signature here is different than what is in Monads. Usually, the value variable is taken as a parameter. The pattern was used to be consistent with Optional, and is effectively equivalent to the partial application of Monad functions where the first parameter is partially applied.

Partial application of the first parameter means that all invocations of the partially applied function will only require the second parameter, and the first parameter’s value will always be whatever was passed in during the partial application. Functionally speaking, this is equivalent to having a private final unchangeable member variable be used as the first parameter to a Monad function.

--

--