Post contents
I dunno about you, but I'm sick of hearing people obsessed with functional programming talking about "monads".
Like, I've been an engineer for years and never understood them.
And I've seen monads explained in a bunch of different ways:
- Visually
- In a 15 minute YouTube video
- Explored in 4 different programming languages
- Compared to burritos
- Visually compared to burritos
- As the memable quote: "a monad is a monoid in the category of endofunctors, what's the problem?"
But none of them quite stuck for me as a predominantly JavaScript-focused engineer.
If I may, allow me to try my hand at this.
Pre-requisite knowledge
I generally assume that you're familiar with JavaScript and Promises. If you're not, I might suggest the following resources:
Introducing Monads in JavaScript
A monad wraps data, adds context & allows for chaining through immutable operations.
In JavaScript, there's one primary built-in API that fits the bill of a Monad perfectly: Promises.
A promise takes some data:
// In JS terms:Promise.resolve(someData);// In TS terms:Promise<SomeDataType>;
And adds some additional context, in this case "time"; a promise may resolve immediately or take some time to resolve the data.
This additional context is stored in a promise as a sort of status
determining the status of the time:
status: "pending" | "fulfilled" | "rejected"
That said, we don't directly interface with status
; We instead interface with this status
through the promise API:
Promise.resolve(12) // Fulfilled .then(x => x * 2) // 24 // Rejected .catch(console.error)
The promise API isn't a one-and-done, either. You can chain then
calls together to process one behavior then another:
Promise.resolve(12) .then(x => x * 2) // 24 .then(y => y + 2); // 26
Each .then
block we create introduces a new Promise
from the previous Promise
in the chain:
const promise1 = Promise.resolve();const promise2 = promise1.then(x => x);console.log(promise1 === promise2); // false
This means that the immutable operation of chaining is the .then
, since each new usage introduces a fresh Promise without changing the previous one.
A note on chained immutability
Astute readers may note that you're still able to mutate the inner value of a monad, like so:
Promise.resolve({current: 12}) .then(x => { setTimeout(() => console.log({x}), 100) return x; }) .then(y => { y.current++; console.log({y}) })
However, this doesn't break the definition of a monad, since the definition applies to the data container not the value being tracked.
Another ecosystem's example
In Rust, they have the
Option
API. This API also describes a monad, since it wraps data and adds an "existance" context.It's either
Some
orNone
:fn main() { println!("{:?}", Some(1)); println!("{:?}", None::<i32>);}
Which can be chained together and implements other monad laws.
Now that we have the basics out of the way, its important to note that monads have three rules they must conform to: The associative law, the left identity law, and the right identity law.
The Associative Law
The "Associative Law" says that monads can chain through nesting:
a().then(x => b(x).then(c))
Or called sequentially:
a().then(b).then(c)
Clarifying associative behavior
When I was first learning about the associative law, I was confusing it with the order of process execution.
Take the following code:
Promise.resolve("123") .then(x => x.split("")) .then(x => x.reverse());
If we re-arrange this to the following:
Promise.resolve("123") .then(x => x.reverse()) .then(x => x.split(""));
We'd instead get the error:
TypeError: x.reverse is not a function
But this is an important distinction: The associativity rule doesn't mean the functions inside the chain are swappable; it means the structure of the chain itself is.
The Left and Right Identity Laws
The "Left Identity" law states that wrapping & immediately unwrapping a monad has no effect:
const f = (x) => Promise.resolve(x);const result1 = Promise.resolve(10).then(f);const result2 = f(10);result1.then(console.log); // 20result2.then(console.log); // 20
Put another way: if you take a plain value, wrap it, and then chain it, the result is the same as just applying the function to the plain value directly.
Think of it as multiplying 1
to a number; it doesn't change the value of the number.
Likewise, the "Right Identity" law states that chaining the wrapping function over a monad doesn't change the outcome:
const originalPromise = Promise.resolve(1);// These two are equivalent:const result1 = originalPromise;const result2 = originalPromise.then(Promise.resolve);
Once again, outlined differently: if you have a monadic value and you chain it with the wrapping function, you get the same monadic value back.
Think of it as multiplying 1
to a number; it still doesn't change the value of the number.
Right identity confusion
Those deeply familiar with JavaScript may know that:
Promise.resolve(Promise.resolve(1))
Unwraps to a single
Promise
containing1
; This to say, multiple promises unwrap to a single level.While this can be helpful in some circumstances, it's not a property inherent to Monads. In fact, Monads should generally not auto-flatten like this.
The "Right Identity" law does not pertain to this behavior. Instead, it speaks to the ability for a monad chain to apply itself without transforming the inner value.
Conclusion
Hopefully this has been a helpful introduction to monads in JavaScript. I plan on doing more with monads, including the "why" behind them and where you might gain benefits from using them in your own projects.
Until next time!
- Corbin C.