November 4, 2024
Monads explained (maybe)
Table of contents
Monads: The Burrito of Programming
As developers, we’ve all heard about monads here and there. You might have heard them described as burritos
or containers
.
For many of us, our first encounter with monads leaves us with more questions than answers. In this article we’ll dive into the world of monads and we’ll try to understand them through some examples.
But never forget the following quote from Douglas Crockford:
Once you understand monads, you lose the ability to explain them to anybody else.
What’s a monad
Let’s start with an “official” definition from wikipedia:
In functional programming, a monad is a structure that combines program fragments (functions) and wraps their return values in a type with additional computation. In addition to defining a wrapping monadic type, monads define two operators: one to wrap a value in the monad type, and another to compose together functions that output values of the monad type (these are known as monadic functions).
General-purpose languages use monads to reduce boilerplate code needed for common operations.
Let’s try to simplify this: a monad is a wrapper around a value that provides a standardized way to chain operations on that value. Think of it as a container that not only holds a value but also knows how to:
- Wrap a value inside itself (
return
orunit
in monad terminology) - Transform the contained value using functions (
bind
orflatMap
in monad terminology).
This might perhaps make you think of a Functor
, and you’re right, monads are functors!
Functors vs Monads
Before diving deeper into monads, let’s understand what a functor is. A functor is a simpler concept that serves as a foundation for understanding monads.
A functor is any type that implements a map
operation, allowing us to apply a function to each item inside the container, producing a new container with the transformed values. For example, JavaScript’s Array is often seen as a functor because its map
method applies a function to each element, returning a new array without altering the original:
A monad is a functor with superpowers. While a functor lets you transform values with regular functions (a -> b
), a monad lets you chain operations that themselves return monadic values (a -> M<b>
). This is done through an additional operation called flatMap
(or bind
).
Here’s a quick comparison:
The key difference is that monads can handle nested structures of the same type and flatten them, which is particularly useful when dealing with sequences of operations that might fail or have side effects.
Notice that if we used map
instead of flatMap
(line 7), we would have ended up with a nested Maybe - Maybe<Maybe<Post[]>>
instead of a Maybe<Post[]>
.
Why is this useful
Recently, I was writing a node.js app that needed to fetch data from a database. Here’s what the code looked like:
Notice all those if
statements checking for undefined
? While this code works, it’s not very elegant and the error handling makes it harder to follow the main logic flow.
Introducing the Option monad
To improve this, we’ll use the Option
monad (that can be known as Maybe
).
The Option
monad represents a value that may not be present, which is exactly what we need!
Option represents an optional value: every Option is either Some and contains a value, or None, and does not.
Here’s how the code looks like when using the Option
monad (for this example, I used the @thames/monads library):
Or even simpler:
Well, this is more readable, right? We’re simply chaining operations using andThen
- which is this library’s name for the monadic flatMap
operation.
We can remove the if
statements because the Option
monad will handle these edge cases for us. How? Let’s see!
We modified our getWorkspaceFromTeam
and getIncidents
functions to return an Option<string>
instead of string | undefined
.
The Option
type can hold two possible values:
Some(value)
: the value is presentNone
: the value is not present
Here’s what getIncidents
looks like:
If None
is returned in the chain of operations, the andThen
method will return None
without calling the next functions. Else, it will return Some(value)
and the next function in the chain will be executed with the unwrapped value as its input.
Actually, the code above can be improved by using a from
or fromNullable
method that takes a value as input (which can be undefined
or null
) and returns an Option<T>
:
This pattern is commonly found in languages like Scala, where Option is a monad that simplifies handling potentially absent values.
I like this quote from this YouTube video (which I highly recommend watching):
Monads are a design pattern that allows a user to chain operations while the monad manages secret work behind the scenes.
This is exactly what we did in the example above. The Option
monad manages the secret work behind the scenes, enabling us to focus on the main logic flow.
Other monads
Either
Either
is almost the same as Option
but with two possible outcomes instead of one. Either
is a monad that represents a value of two possible types: Left
or Right
. It’s often used to represent computations that may fail and can return an error.
If we make a parallel with the Option
monad, Right
is typically used for success cases (like Some
) and Left
for failure cases (like None
), but both Left
and Right
can contain values.
It is convenient to use the Either
monad when you want to return an error message. If an exception is thrown, you can return an Either.left(error)
. When using flatMap
, if any operation in the chain returns a Left
, the subsequent operations are skipped and the error value is preserved.
Let’s see how we can rewrite the previous example using the Either
monad. First, we have to modify our getIncidents
and getWorkspaceFromTeam
functions to return an Either<Error, T>
.
Here’s how getIncidents
might look like:
You may have noticed that the code is very similar to the Option
monad example. The only difference is that instead of returning None
, we return Left
containing an error message.
We can then modify our main function getIncidentUpdatedView
to use the Either
monad:
Notice how we used the rightAndThen
method to chain the operations. This method enables us to chain operations on the unwrapped Right
value of the previous operation. If an operation in this flow returns a Left
, the subsequent rightAndThen
operations are skipped and the Left
value is preserved through the rest of the chain.
Finally, we used the match
method to display a log message based on the result.
Future
Future is a monad that represents a value that may be available in the future (yes, like promises). As before, we can chain operations with methods such as andThen
or flatMap
that will wait for the previous operation to resolve before applying the next one.
An example would be:
fork
is used to execute the Future
and provide callbacks for handling the result and error.
The Future monad is particularly useful for handling asynchronous operations in a functional way. Unlike Promises
, Futures are lazy - they don’t start executing until fork
is called. This gives us more control over when our asynchronous operations begin and allows for better composition of async logic.
Conclusion
Of course, monads are much more than what we’ve seen in this article. This article is an introduction and we’ve only scratched the surface. I don’t pretend myself to fully understand everything about monads, but I hope this article will help you understand them better.
Monads really are design patterns that help us handle common programming scenarios in a more elegant way. We’ve seen that:
Option/Maybe
helps us handle nullable valuesEither
helps us handle errors with additional contextFuture
helps us handle asynchronous operations
But of course, there are many other monads that can be used in different scenarios.
Remember: You don’t have to use monads everywhere. Start small, perhaps with Option/Maybe
for nullable values, and gradually incorporate other monadic patterns as you become comfortable with the concept.
Resources I recommend to go further
- This YouTube video is a great introduction to monads.