A monad is a concept originating from a part of mathematics called category theory, not a class or trait. In this article, I will try to explain its structure and inner workings.
With the use of Optional from Java, I will try to describe all of this in a more understandable way.
I will also implement a basic monad to better understand how they work and conclude with a short usage example to show the monad’s advantage over the non-monad approach.
The source code for this article is available in GitHub repository.
Why Learn How Monads Work?
First of all, it is always good to have a basic understanding of how things that we use work. If you are a Java developer, you probably use monads and may even not know it. It may surprise you but two of the best known Java 8 features, namely Stream and Optional are monad implementations.
In addition, functional programming is becoming more and more popular nowadays, so it is possible that we will have more similar monadic structures. In such a case, knowing what a monad is and how it works may become even more valuable.
If you also want to learn something more about Optional history I can recommend you another one of my texts in which I am describing changes in Optional API since its introduction.
Let’s start with describing what a monad is — more or less accurately. In my opinion, the matter here is fairly straightforward.
Monad is just a monoid in the category of endofunctors
Based on a quote from “Categories for the Working Mathematician” by Saunders Mac Lane.
Back to being serious…
What Is Monad?
After reading the intro, you know that a monad is a concept from category theory. In the world of software, it can be implemented as a class or trait in any statically typed language with generics support.
Moreover, we can view it as a wrapper that puts our value in some context and allows us to perform operations, specifically operations that return value wrapped in the same context, on the value.
Also, we can chain the operations in such a manner that the output of an operation at any step is the input to the operation at the next step.
Examples of monads in modern-day programming languages:
- Stream (Java).
- Optional/Option (Java/Scala).
- Either (Scala).
- Try (Scala).
- IO Monad (Haskell).
Monad Laws
The last thing that needs mentioning while speaking of monads is their laws.
If we want to consider our implementation a real monad, we must obey them. There are three laws:
Left Identity, Right Identity, and Associativity.
In my opinion, it can be somewhat hard to understand what they actually mean.
Now, with the help of Optional, I will try to explain the above laws in a more detailed way.
But first a few assumptions:
- f is a function mapping from type T to type Optional<R>
- g is a function mapping from type R to type Optional<U>
-
Left identity
If we create a new monad and bind it to the function, the result should be the same as applying the function to the value.
Optional<String> leftIdentity = Optional.of(x).flatMap(f); Optional<String> mappedX = f.apply(x); assert leftIdentity.equals(mappedX);
- Right identity
The result of binding a unit function to a monad should be the same as the creation of a new monad.Optional<Integer> rightIdentity = Optional.of(x).flatMap(Optional::of); Optional<Integer> wrappedX = Optional.of(x); assert rightIdentity.equals(wrappedX);
- Associativity
In the chain of function applications, it should not matter how functions are nested.Optional<Long> leftSide = Optional.of(x).flatMap(f).flatMap(g); Optional<Long> rightSide = Optional.of(x).flatMap(v -> f.apply(v).flatMap(g)); assert leftSide.equals(rightSide);
If you are enjoying reading about Monads and want to learn similarly related concepts, I recommend you read the rest of the articles from my functional containers list.
Creating a Monad
Now, when we know the basics, we can focus on implementation.
The first thing we need is a parameterized type M<A>, which is a wrapper for our value of type A. Our type must implement two functions:
- of (unit) which is used to wrap our value and has the following signature
M<A>(A). - flatMap (bind) responsible for performing operations. Here we pass a function that operates on value in our context and returns it with another type already wrapped in context. This method should have the following signature M<B> (A -> M<B>).
To make it more understandable, I will use Optional one more time and show what the above structure looks like in this case.
Here, the first condition is met right away because Optional is a parameterized type. The role of the unit function is fulfilled by ofNullable and of methods. FlatMap plays the role of the bind function.
Of course, in the case of Optional, type boundaries allow us to use more complex types than in the definition above.
Implementing a Monad
package org.pasksoftware.monad.example; import java.util.function.Function; public final class WrapperMonad { private final A value; private WrapperMonad(A value) { this.value = value; } static WrapperMonad of(A value) { return new WrapperMonad<>(value); } WrapperMonad flatMap(Function<A, WrapperMonad> f) { return f.apply(value); } // For sake of asserting in Example boolean valueEquals(A x) { return value.equals(x); } }
Et voila, monad implemented. Let’s describe in detail what exactly I have done here.
The base of our implementation is the parameterized class with the immutable field named “value”, which is responsible for storing our value. Then, we have a private constructor, which makes it impossible to create an object in any other way than through our wrapping method — of.
Next, we have two basic monad functions, namely of (equivalent of unit) and flatMap (equivalent of bind), which will guarantee that our implementation fulfills the required conditions in the form of monad laws.
With the functionalities described, it is time for a usage example. So here it is.
package org.pasksoftware.monad.example; import java.util.function.Function; public class Example { public static void main(String[] args) { int x = 2; // Task: performing operation, returning wrapped value, over the value inside the container object. // Non-Monad Function<Integer, Wrapper> toString = i -> new Wrapper<>(i.toString()); Function<String, Wrapper> hashCode = str -> new Wrapper<>(str.hashCode()); Wrapper wrapper = new Wrapper<>(x); Wrapper stringifyWrapper = toString.apply(wrapper.value); // One liner - Wrapper hashCodedWrapper = hashCode.apply(toString.apply(x).value); Wrapper hashCodedWrapper = hashCode.apply(stringifyWrapper.value); // Monad Function<Integer, WrapperMonad> toStringM = i -> WrapperMonad.of(i.toString()); Function<String, WrapperMonad> hashCodeM = str -> WrapperMonad.of(str.hashCode()); WrapperMonad hashCodedWrapperMonadic = WrapperMonad.of(x) .flatMap(toStringM) .flatMap(hashCodeM); assert hashCodedWrapperMonadic.valueEquals(hashCodedWrapper.value); System.out.println("Values inside wrappers are equal"); } }
In the code above, apart from seeing how monads work, we can also see a few pros of using them.
In the monadic part, all operations are combined into a single execution pipeline which makes the code more declarative, easier to read and understand. Additionally, if we decided some day to add error handling logic, it can be nicely encapsulated inside of and flatMap methods.
On the other hand, in the non-monadic part of the example, we have a different design with package-private field value, we need a way to access value from outside the wrapper, which breaks the encapsulation.
As for now, the snippet is readable enough, but you may notice that it is not extension-friendly. Adding any kind of exception handling may make it quite spaghetti.
Summing up
Monad is a very useful and powerful concept and probably many of us use it in our day-to-day work.
It nicely handles nested containers and provides a clear descriptive way of describing pipeline of operations. In years to come it will appear more and more frequent in the Java ecosystem.
Thank you for your time.
Comments are closed.