Skip to content

Instantly share code, notes, and snippets.

@beardedtim
Last active July 18, 2022 16:40
Show Gist options
  • Save beardedtim/a025d38c85e71900375115b77c4b8ba1 to your computer and use it in GitHub Desktop.
Save beardedtim/a025d38c85e71900375115b77c4b8ba1 to your computer and use it in GitHub Desktop.
Monads are like burritos...and trees...and clocks...and...

Monads and Why The Hell We Need Weird Words

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

Table of Contents


The Monad Rules

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 a map and flatMap/chain method in order to create a new Monad based on the wrapped value and static methods of unit/of in order to create a new Monad of the passed in value

That's a mouthfull. Let's look at each of those requirements individually

Monads Can Be Mapped

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!

Monads Can Be FlatMapped

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 Can Be Created Without Client Instantion

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)
  }
}

Common Examples

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 Promises 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:

Why This Matters

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!


Shadows on the Cave Wall

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment