What the hell are monads you ask?
The formal wikipedia definition says:
“In functional programming, a monad is an abstraction that allows structuring programs generically. Supporting languages may use monads to abstract away boilerplate code needed by the program logic. Monads achieve this by providing their own data type (a particular type for each type of monad), which represents a specific form of computation, along with one procedure to wrap values of any basic type within the monad (yielding a monadic value) and another to compose functions that output monadic values (called monadic functions).”
It is said that there’s a curse with Monads. I’m not making this up and it’s called the “monad tutorial fallacy” and the legend says that when you finally understand them, you lose the ability to explain it to others. The curse is actually described in 4 steps:
- person X doesn’t understand monads
- person X works long and hard, and groks monads
- person X experiences amazing feeling of enlightenment, wonders why others are not similarly enlightened
- person X gives horrible, incomplete, inaccurate, oversimplified, and confusing explanation of monads to others which probably makes them think that monads are stupid, dumb, worthless, overcomplicated, unnecessary, something incorrect, or a mental wank-ercise
Great motivation … So why write about this beast?
Monads are building blocks in the functional programming world and we see that functional programming is integrated to some extent in most modern object oriented programming languages.
Imagine you are an object oriented programmer in a Java 6 world. Our code is a representation of logic and how many times do you find below operations in your code:
- Operations that work on nullable/optional values
- Operations that return a value or an error
- Operations that perform operations on a List/Array
- Operations that have an asynchronous nature.
Automatically you are responsible for coding below logic:
- Execute the rest of the code only if the value isn’t null
- Execute the rest of the code only if there was no error
- Execute the rest of the code once for each value in the list
- Execute the rest of the code once the asynchronous code is finished
I see a repeating pattern emerging …
Wouldn’t it be nice if you could just focus on the core of your logic without worrying about generic logic like “executing the rest of the code once for each value in the list”.
Wait a second …
Java 8 is influenced by the functional programming world and introduced concepts like Streams on Lists and Optional. Those concepts seem to fit in the description above …
Lists / Options / Futures are monads and you’ve been using their Java equivalents without even knowing they were monads.
Ok so you have this concept and why should I care about the internals of these concepts? I use some implementations, I like those implementations and I don’t have to understand them completely to take advantage of their power.
My challenge is twofold here:
- Break the curse and explain what the hell a monad is
- Give insights why it’s a powerful concept that is so much more than the implementations most object oriented programmers are familiar with (Java Optional, etc)
I learned the concept of monads and it gave me a very different perspective on concepts that I took for granted for many years. The new perspective influenced my code in many ways. (whether it was Java or Scala or Python)
Monads are everywhere but it doesn’t mean that we need to do anything special with them. You can use them, you can spot them, or completely ignore them and you will be fine.
But when dealing with large amounts of data or with complex data modelling, I immediately noticed the advantages of taking the monad perspective into consideration when deciding how to write code. Monads are not the answer to every problem but understanding them will give you a different perspective on subtle things and you will think about every “if-check” in your code in new ways.
This new way of thinking promotes code that is easier to test / more robust / more suitable for parallel / concurrent programming. Monads is a concept that is entangled with functional programming concepts like pure functions, higher order functions, immutability, etc and many of those concepts contribute to your code quality.
Almost all the code and the examples in this blogpost are written in Scala. Why Scala you ask?
- Scala is the preferred programming language for many data engineers.
- Scala can make senior developers feel very junior. Old programmers like me like to feel young again.
- Scala is sexy
I don’t want to hurt the feelings of Kotlin fanboys, but always remember that Kotlin is a better Java and Scala is a more powerful Java …
Back to monads?
Let’s get started and do a conceptual dive into these mysterious monads.
Informally, a monad is anything with a constructor and a flatMap method and it’s a mechanism for sequencing computations …
Well that’s it?
This is the core that you need to keep remembering and I will guide you step by step through the above statement.
But before we jump to monads, we have to talk about functors first. Functors? What is that?
Monads and Functors are both wrapper concepts that have roots in the same math theory (https://en.wikipedia.org/wiki/Category_theory). We won’t go into the mathematical details, but think of both concepts as wrappers. The wrapper can be represented as a box around something. In the below image you can see a box that can contain an Integer value.
The wrapper concept is pretty useless if we are not allowed to do operations on the wrapper. In the below example you can see that we can put a value in the box and can apply a transformation on the value inside the box (if it exists).
Functors and monads should both provide ways to put something inside the box. The difference between both is that you can call the map() function on the functor and you can call the flatmap() function on the monad. In the above image we have a box that may contain a value. In the Java world this is an Optional (Option in Scala).
Let that sink in …
- Yes, a Java Optional is a functor because it has a Map function and it’s a wrapper concept.
- Yes, a Java Optional is a monad because it has a FlapMap function and it’s a wrapper concept.
Ok, so our definition so far is pretty simple. A functor is a wrapper that has a map function and a monad is a wrapper with a flatmap function. Because flatmap is a special map function (= you “map” first and then you “flatten”), we can also conclude that every monad is also a functor.
Are we there yet? No keep hanging in there …
Because monads have mathematical roots, we need to understand the laws that apply to monads. Let’s formalise the box / map / flatmap principles :
- We need a Unit function. This function allows us to put a type A in a box F
def unit: A → F[A]
- We need Map functionality, that allows us to apply A → B on the A type inside box F
def map: F[A] → (A → B) → F[B]
- We need a Flatten functionality that allows us to unpack a type A inside 2 boxes F.
def flatten: F[F[A]] → F[A]
With these three principles in mind we can start to write a formal Scala definition of these principles.
- We need a unit function that takes a parameter of type A and puts it inside the wrapper M.
- Our M type has a trait (interface in JAVA) that says that we need a FlapMap function on our wrapper type. That FlapMap function takes a function as parameter and that function maps a type A to a type B (wrapped inside our wrapper/monad type).
Why is there no Map in the above Scala code? Remember that every flatmap is a special case of a map function. This means that every Map operation can be rewritten as a FlapMap operation. So we don’t need to specify this for our monads in the Scala trait (=Java Interface).
So where’s the math in all of this?
Monads have mathematical laws that need to be obeyed by the unit function (=put something in the box) and the flatmap function. These laws are described below:
unit(x).flatMap(f) == f(x)
m.flatMap(unit) == m
m.flatMap(f).flatMap(g) == m.flatMap(x ⇒ f(x).flatMap(g))
We can see this in practice in the below Scala code which checks the monad laws for the Scala Option monad. Examine the code at your own pace …
This gives us the following output :
Let's dive into the world of monads laws ...
left identity law -> both below statement are equal
right identity law -> both below statement are equal
associativity law -> both below statement are equal
I hear you thinking. This is all great, but I don’t see what’s the fuss all about…
In the functional programming world it’s all about functions. Writing the functions is usually not the hard part, but then we have to glue them together in a functional world. Remember our first statement: “a monad is anything with a constructor and a flatMap method and it’s a mechanism for sequencing computations”.
So let’s first do some sequencing. We will create a user service that retrieves a user object. That user object may have a child (recursive data). I want to see if our user has a grandchild …
This gives us the following output :
Let's do some sequencing ...
This is great as the flatmap operator allows us to chain operations together and to keep doing that for as long as we want. This shows the advantage of flatmap over a map function.
- Using the map operator would allow us to do one map operation and then we run into trouble …
- Using the flatmap operator you can see that our flatmap function remains very clean.
This is a principle that generalises very well. The above example is very simple but how does it help us in chaining different functions. Remember that we are in a functional programming world. We like to use pure functions that have no side effects. Below you can see 2 pure functions.
This gives us the following output :
the monad way ... Some(2) None None
The semantics of the stringDivideBy method:
- the first call to parseInt returns a None or a Some;
- if it returns a Some, the flatMap method calls our function and passes us the integer aNum;
- the second call to parseInt returns a None or a Some;
- if it returns a Some, the flatMap method calls our function and passes us bNum;
- the call to divide returns a None or a Some, which is our result.
At each step, flatMap chooses whether to call our function, and our function generates the next computation in the sequence.
In other words. It stops the chain when it becomes useless to continue. We are not interested in knowing where our chain failed. We are interested in whether our complete chain succeeded or not.
Ok, so we have a mechanism that allows us to chain clean pure simple functions which are readable and can be tested extremely well (no mocking required). We are able to sequence operations on our Option monad and on functions that return our Option monad.
This sounds very promising, but I already use the “standard” monads like Optional in Java 8. I don’t see the benefits yet in exploring the Monad world any further? Let’s write a monad ourselves for a real problem situation. Suppose you want to combine several functions and keep track of all the operations that are done (a log trail for profiling purposes for instance). Not a real hard problem and it can be solved in numerous ways but today we will choose a custom monad implementation.
We first create our Monad Class Debuggable. It’s a monad so what do we need?
- A constructor
- A FlapMap function
- An optional Map function
In Scala you don’t need to specify that constructor and we create a case class that holds a value (the operational value) and a message (the log trail). What else do we need?
- We create our Map function and this just executes the provided parameter function on the value and wraps it inside a new Debuggable object.
- We create our FlatMap function and remember that we will use this for sequencing a chain of operations. It’s identical to our map implementation but now we append the message instead of replacing it.
We seem to have a custom monad. Now let’s create some functions that use our monad (they are all pure functions that return our monad).
How do we glue the monad and the function together so that we can combine those 3 functions in any way we want and we will get a result and a log trail of our intermediate results.
Remember that we use the flatmap for sequencing and we use the map in the last step, because this is where the sequencing ends.
This gives us the following results:
- final result :
- log message :
f: input: 100, result: 101 g: input: 101, result: 103 h: input: 103, result: 106
This is a big step and you can let that sink in … You can create your own monads for your specific needs. The functions above didn’t care how they were combined and in what position they were executed. They just knew that they needed to return a Debuggable object.
Scala provides syntax that hides the ugly flatmap chaining constructions and you can write it more concise like this:
This gives us the following output :
final Scala for comprehension value: 106 final Scala for comprehension msg: f: input: 100, result: 101 g: input: 101, result: 103 h: input: 103, result: 106
In the monad world you just witnessed a writer monad (https://en.wikipedia.org/wiki/Monad_(functional_programming)#Writer_monad). The code was not perfect and the code was also not very reusable (you have to return a Debuggable object in your pure functions) but it shows that you can write custom monads for your own needs.
The imperative solution to this problem is way simpler. Why prefer the monad?
Let’s say we start to profile code that is using multiple threads. Standard imperative logging technisch can result in interleaved messages from multiple threads. The writer monad doesn’t have this problem and using the monad version will actually simplify your solution. Remember that functional programming shines in the parallel / concurrent programming world.
This is just a very specific use case and there are many problem patterns that can be solved with Monads. Think of them as one of the gang of four patterns (https://www.gofpatterns.com/) for the functional programming world. Absorb these best practices and decide if the monad implementation is the best fit for your use case.
A good monad can take in pure clean functions that are robust / easy to test / easy to read / etc. Those clean functions can be used in a monad implementation or in a non monad implementation. The monad should take away the boilerplate context for you. You can write code and use it in the Future Monad context and reuse it in another Monad context or even use it in a non Monad context.
I hope you now have a feeling of what Monads are and why they matter in certain software development contexts.
Kristof Slechten behaalde een master Informatica aan de VUB en is gespecialiseerd in projecten die betrekking hebben op big data & machine learning. Momenteel is Kristof aan de slag bij Imes Dexis waar hij onderzoekstrajecten rond machine learning uitwerkt. Daarnaast werkt Kristof mee aan verschillende interne onderzoeksprojecten rond AI.