Once you understand what monads are, and why they exist, you lose the ability to explain it to anybody.
Here's to hoping I don't understand them
I think most people that use the word Monad non-ironically can agree with the definition of Monad given by fantasy-land which can be expanded and generalized into:
A
Monad
is some structure that wraps a value and offers amap
andflatMap
/chain
method in order to create a newMonad
based on the wrapped value and static methods ofunit
/of
in order to create a newMonad
of the passed in value
That's a mouthfull. Let's look at each of those requirements individually
A Monad is some structure that wraps a value. Very OOP of us, I know. One of the methods
that this structure offers us is called map
. Let's look at its usage before talking about
how it works:
const monad = new Monad(1)
const otherMonad = monad.map(num => num * 2)
monad // monad wrapping the value 1
otherMonad // monad wrapping the value 1 * 2 or 2
Somehow we create a Monad
that wraps the value of 1
. Since it is a Monad
, it offers
the method map
which, whatever it does, returns a new Monad
, without affecting the
original Monad
.
How might we create that Monad
class? A naive implementation might be
class Monad {
constructor(value) {
this._val = value
}
map(fn) {
return new Monad(fn(this._val))
}
}
Some structure (class
) that wraps the value this._val
. This structure offers a method
called map
that takes some function and returns a new Monad
with the value being wrapped
set to whatever fn
returns.
What if the value that fn
returns is, itself a Monad
? That's where flatMap
comes in!
If we have Monad(Monad(1))
, how can we map
the internal value 1
into 2
without
having to know that 1
was wrapped by a Monad
? Put it another way, if we want to write
our business logic without unwrapping the inner Monad
? How can we ask the outer Monad
to
flatten the internal value and then apply some function? We can use the flatMap
method.
Before we see how to use flatMap
, let's look at some problem that it helps solve
// Our super secret, awesome special algorithm
// that returns a _wrapped_ value
const doubleMonad = num => new Monad(num * 2)
// We create our original _wrapped_ value
const monad = new Monad(1)
// And we want to _map_ it into a new value
// but that _new_ value is a Monad meaning
// that doubled is Monad(Monad(2))
const doubled = monad.map(doubleMonad)
How can we get doubled
to be Monad(value)
instead of having that returned Monad
trouble us? flatMap
,
also known as chain
to the rescue:
const doubleMonad = num => new Monad(num * 2)
const monad = new Monad(1)
// Now doubled is Monad(2) because flatMap
// _flattened_ the _wrapped_ value into a
// single Monad
const doubled = monad.flatMap(doubleMonad)
Now our business logic that returns a Monad
doesn't have to worry about unwrapping its internal value and our
consumer code doesn't have to deal with nested wrappings of values.
Once again, let's look at what this might look like inside of our simple Monad
class
class Monad {
/* ... previous code ... */
static lift(monad){
return monad._val
}
flatMap(fn) {
return Monad.lift(this.map(fn))
}
}
This naive approach calls map
and then lifts the value out of the Monad. Now we can deal with business logic
that, itself, returns Monads
.
What if I don't want to call new
but still want to create a unit
or new instance of
a Monad
?
Monads
offer a static way to create a new Monad
called either unit
or of
which, as its name suggestions,
returns a new Monad
const monad = Monad.of(1)
Monad.lift(monad) // 1
With its implementation looking like
class Monad {
/* ... previous code ... */
static of(value) {
return new Monad(value)
}
}
JavaScript the language has Monads
in spec, if you squint your eyes. For example, we can map
an array
const arr = [1, 2, 3]
const newArr = arr.map(num => num * 2)
It even offers unit
/of
const arr = Array.from({ length: 3 }, (_, i) => i + 1)
Admittedly, it's not as pretty as our generic class above. But it offers these capabilities. How about
flatMap
?
const arr = [1, 2, 3]
const doubleToArr = num => [num * 2]
const doubled = arr.flatMap(doubleToArr)
Yup! It follows all three laws (map
, flatMap
, and unit
) so it is a Monad
! What are some other types
that JavaScript offers? Well, if you really squitn, like super hard we can categorize Promise
s as a Monad
:
// Promise(1) sure looks like Monad(1)
const prom = new Promise(res => res(1))
// Can I map it?
prom.map(num => num * 2) // ERROR! NO MAP!
// How about flatMap?
prom.flatMap(num => new Promis(res => res(num *2))) // ERROR! NO FLAT MAP!
What?! It doesn't offer map
or flatMap
! How can it be a Monad
?! Because it offers the ability to map
and flatMap
, but just calls it differnt things: then
and catch
.
As a refresher:
const getUser = () => new Promise(res => res(1))
getUser()
.then(console.log)
will eventually print 1
whenever res(1)
gets called. What do I mean by that?
const getUser = () => new Promise(res => {
setTimeout(() => res(1), 500)
})
getUser()
.then(console.log)
console.log("I am reading and executing the file _after_ getUser().then called")
This will cause I am reading...
to be printed around 500ms
before we print 1
. That is because we don't
call res(1)
until 500ms
after we create the Promise
.
What we can do inside of then
makes it Monad
-ish:
const double = num => num * 2
const prom = new Promise(res => {
setTimeout(() => res(1), 500)
})
const doubled = prom.then(double)
prom.then(num => console.log(`${num}: ORIGINAL`)) // 1: ORIGINAL
doubled.then(num => console.log(`${num}: DOUBLED`)) // 2: DOUBLED
We can pass in a function to then
and it will return a new Promise that has the function applied to its
wrapped value. Put it another way, calling .then
on a Promise maps the value into a new Promise via the
passed in function. Very similar to how we described a Monad
!
How about flatMap
? How does a Promise
offer us that ability? With the same method!
const getUser = (id) => new Promise(res => res(id))
const getFriend = (id) => new Promise(res => res(id + 1))
const user1Friend = getUser(1) // this is a Promise
.then(user => getFriend(user)) // getFriend returns a Promise
We can return a promise from the function we pass to then
and it will resolve that promise into a
value before moving on, just like flatMap
does for Monads
. Given that we can flatMap values, that's
another check for a Promise
being a Monad
.
How about the last rule Monads
have to follow, the static method unit
/of
? It calls it resolve
:
const prom = Promise.resolve(1)
prom.then(console.log) // 1
So just like we can map
, flatMap
, and unit
an array in JavaScript, we can do the same (remember, you
are squinting like a pro by this point) with Promises.
On top of the language itself, there are numerous packages that offer these functions that either work on Monads or Monad-like structures (ramda, lodash, underscore) or on custom structures (rxjs, most, bacon). And that fact alone should point at our next section:
Theology is all good and well and we can squint all we want to create relations where none should be. But why
is this helpful? How can we use this in our day-to-day explorations of code? In my eyes, it offers what
I have been craving: general abstractions over data and manipulation. Not only do I want to be able to
express logic that manipulates input of type a
into type b
as generically as possible, I want to talk about
these transformations generically. What do I mean by that?
Suppose that I have some algorithm: num => num * 2
.
I don't care who or what gives me num
. I only care that, given some num
, I can return num * 2
. That is
all I care about. There is nothing about where that num
comes from. No mention of it being stored in a DB
or in global state or from a user input. There is also no mention of where that values goes. It doesn't store
it in a DB or output it to console. It just returns it.
This type of business logic is pure and composable, which are my personal rules of functional code (a topic
for another day). I can test this easily (just give it a number and assert
the return value). I can re-write
this without worrying about reprocussions elsewhere throughout my system. I can reason about this code simply by
looking at the code itself; I don't need any other context to know what the return value will be.
I don't care if num
came from memory or the DB in my algorithm but at some point I will need to, right?
At some point, we can't be pure. At some point, we have to do something that isn't num * 2
in order to
get num
there in the first place. Is all lost? Do I lose the composability of functional code? Can I not
test the full code without making those DB calls or outputting the response? Not. At. All
Remember how we said that, if we squint real hard, Promises are Monads
? Let's think about what that means in
light of the above. If I have some function that calls the DB, how can I test that the other parts of my code
work with its eventual value? How can I write the other parts of my code in a pure, composable way? That's
what Promises/Monads
are made for!
Let's take an example from above and change the result of calling getUser
and getFriend
. By that I mean,
instead of just returning a Promise ourselves, let's make some DB call
// Given some ID, return a Promise of a user
// OLD: const getUser = () => new Promise(res => res(1))
const getUser = id => axios(`/api/users/${id}`)
// Given some user, return a Promise of its friend
// OLD: const getFriend = () => new Promise(res => res(2))
const getFriend = user => axios(`/api/users/${id + 1}`)
const user1Friend = getUser(1).then(getFriend)
Even though the wrapped value changes (instead of it wrapping a hard computed value, it wraps the value of an
api call), our business logic of getUser().then(getFriend)
stays the exact same. We can implement getUser
however in the hell we please as long as it returns a Monad
(Promise). Wow, that sounds hella like OOP!
And the shadow I am pointing at, with all of this, is that a Monad
wraps a concept. It can wrap the DB call,
the hard coding of a new value, or even events over time. The value or concept doesn't matter. What
matters is that it offers us the ability to map from one domain of that concept into a range of another
concept, even if that concept is, itself, a nested concept. Whoa. Concept doesn't mean anything now. What the
hell am I talking about?
If we think of our problems in terms of generic algebraic types, we get the OOP A is a B type of grouping
but without the banana/gorilla
problem that plagues Object Oriented paradigms. We can say that Monad.of(A)
returns Monad(A)
without caring what concept we are wrapping A in. Which means we can change events over time in our production
code to a hard coded value at test time and my business logic doesn't change. It just happily calls map
or
flatMap
. My business logic does not care that Monad
wraps the value in the concept of time.
We can explain to our worker how to transform values instead of understanding values. We can explain how these
transformations should take place without knowing where these values come from. Think on that for a second.
Using Monads
you no longer care if the data you need is in a DB, a JSON file, or in memory. You no longer care
about the wrapper of the value. And the biggest shadow of them all is that everything outside of numbers is a wrapper of a value when it comes to computers.