Functional programming with Java Monads.
What is a monad?
- Implements flatMap (a.k.a. bind) and unit (a.k.a. identity, return, Optional.of(), etc…).
- Follows three laws: Left identity, Right identity and associativity, which are out of the scope of this post.
For the sake of aligning the terminology, let’s say that a parameterised type such as Optional<String> has a type parameter: String and a wrapper type: Optional. In this case we say that “a String type is wrapped by an Optionalcontext”.
How are monads useful?
Monads are all about composition of parametrised types. Let’s take the Optional monad as an example. We know that to be able to manipulate the parameter properly, we should at least check whether the optional is not empty and then get the value out of it. Also, it’s a good practice to do something in case the Optional is empty. We’ll call this entire process “unwrapping the parameter from the Optional context”.
Let’s write some code. First, let’s try adding two Optional numbers. If one of them is empty, then return empty, otherwise add them and return an Optional. Classic.
Notice how we had to manually “unwrap the Integer from the Optional context”by checking whether the numbers are present (line 2), and doing something with the empty case (line6).
This is precisely what monads helps us to do: To avoid dealing with the context when composing parametrised types. In the next example, the context refers to the Optional context, and composition refers to addition, so we can say: The Optional monad help us to avoid dealing with the Optional context when adding Optional types.
They do so thanks to the methods flatMap (a.k.a bind) and Optional.of (a.k.a unit). The flatMap method let us take the parameter from the monad and operate with it to produce another Monad of the same type. Because of this signature, we can nest flatMap calls on different monads to compose their parameters. Also, Optional.of (a.k.a unit) is a very handy method that will let us take any value and produce another Optional. Look at this:
1 — Bear with me with the weird indentation, it’s for didactical purposes. I will present an alternative later in this post.
2 — The flatMap method receives a Function. We’ll call this function the “mapper” function.
Notice how we didn’t need to check whether the values are present and deal with the empty case. The Optional monad via its flatMap method properly unwraps the parameter for us. It won’t invoke the second flatMap mapper function if the first Optional is empty. In fact, if any of the Optionals involved in the composition is empty, the result will be an empty Optional.
Also look how “val2.flatMap” (line 4) is nested inside the mapper function of “val1.flatMap” (line 3). This is because the main operation “first + second” (line 5) needed two parameters from two different Optionals. The mapper function of the nested Optional (val2) will have both parameters in scope.
Now, this is more than just syntax sugar (although it looks like it is because the Optional monad is a very simple one), this approach actually allow us to deal with the type parameter of monads without even needing to know how to unwrap it. We didn’t have to think what to do with the empty case, or how do we get the value out of the monad, or what would happen if we ever dare to get the value of an empty Optional. This is knowledge that optionalAddmethod doesn’t need to have to be able to compose the monads. Such property becomes more important when unwrapping a monad parameter is not so simple. I will emphasise on this when we discuss the Result monad in the next post.
Meanwhile, let’s discuss another Optional example. We all know that nested Optionals can get a bit cumbersome to handle. Look at this:
Here we have in lines 4 and 5, two methods that return an Optional<Counter> monad and we want to calculate the total colourCountvalue of the two counters.
Look at the totalColourCount method (starting on line 8). To access the colourCount property of an Optional<Counter>, I need to unwrap Counterand then unwrap colourCount (line 13–19).
With flatMap and a bit of Java 8 syntax sugar, we can avoid handling the Optional context.
You can find the complete implementation of the Optional examples here and some tests showing the different scenarios here.
Wrapping it up
Monads are types that have a standard way of composition in which the context manipulation is handled by the Monad itself. It is standard because any parametrised type that satisfies the monad laws can be composed in the same way.
In Java, the Optional type follows all the monad laws and by nesting and chaining its flatMap method we can compose it nicely. Like Optional, Java provides other monadic types such as Stream and CompletableFuture. They behave in similar way to Optional in that they follow the monad laws and so, they are equally composable.
In the next post I will try to show more explicitly the impact of not having to deal with the context by discussing the Result monad. Also we will analyse how it can be used as an alternative to Java checked exceptions. We will then add side-effects-free logging capabilities to it and modify the flatMap method to be able to support composition of the log and comply with the monads laws.