home
Functors and monads in javascript
Trying the impossible. Explain functors and monads in the context of javascript and how we are benefitting from them.
Sep 30, 2020
#react

I embark on a journey to explain what functors and monads are. No category theory. No abstract explanation. Just what they are and how you can benefit from them.

So beware, you have been using, both of them, sort of.

What are they?

Among many other algebraic data type.... Hooooohohooooo, stop there dude. You just said no mambo-jumbo.

Clears throat, fair enough.

They are containers of some sort. Functors and monads are containers, among many others. Think of a value. Like a primitive. The number 2.

We can put that into a box, a container, a wrapper. Name it. And than the box can do something with that value.

We start with functors because they are easier to comprehend. Functor is such a box. You can put a value in it and the functor will provide a functionality. This functionality is the map function.

This is going to be the same with monads. Monads are a box, a container, that provides different functionality, that is called flatMap or chain (I think it is also bind in some languages).

But let's not run ahead. I just wanted to demystify these things. We have containers that provide functionality or in other words, abstract away some logic.

Now, ES6+ veterans know we have a map function on the array. Yes, that makes arrays functors. Arrays are functors because they implement the map interface.

So what does map do? map takes a function (callback) and will return another array.

copy
js
const arr = [1];
const newArr = arr.map(x => x + 1); // [2]

We all know what it does. map iterates over the array, increment all the numbers by one, and returns a new array (immutability).

But we do not know how that is done. We just use it, and magic. That is it. That is a functor.

To remove the illusion just like when we realized Santa is not real, functor can be anything.

copy
ts
interface Identity<A> {
map: <B>(fn: (a: A) => B) => Identity<B>;
value: () => A;
}
export function Identity<A>(value: A): Identity<A> {
return {
map(fn) {
return Identity(fn(value));
},
value: () => value,
};
}
Identity.of =<T> (x: T) => Identity<T>(x);
const idFunctor = Identity.of(5); // Identity(5);
const newFunctor = idFunctor.map(x => x + 1); // Identity(6);

This is a very small and simple functor. Why? Because it has a map function. Identity is mappable.

Apart from map it has a few more functions. Just utility things. The first is the value function. value just unwraps the value from the functor.

Functor that holds 5 can be mapped, but not very useful otherwise. We want to get that out, so we can perform some side effects with that for example. We can not send a functor to our database but we can send 5 for sure.

copy
ts
const idFunctor = Identity.of(5); // Identity(5);
idFunctor.value(); // 5;

The other is the static property of. It is the reverse of value. It puts a value to the context of the functor. Array has the same thing, however it is not called of.

copy
ts
5; // value
[5]; // values lifted to context
new Array(5) // values lifted to context

These thing will put the value to the context of the Array. Array is mappable, a number is not. The fancy name of putting a value to the context is called lifting. We lift the value to the context of functor.

Where else?

Where else we have seen this pattern?

copy
ts
const promise = Promise.resolve(5); // Promise(5)

Yes that is true. We can lift a value into the context of the promise. The story does not end here.

copy
ts
promise.then(value => value + 1); // Promise(6);

Yes, yes promises are functors. Then can be mapped, just the map method is called then. Actually it is a bit more complicated, we'll see it in a sec. But you can map over a value, if that is lifted into a Promise.

Promise, as a functor, abstract away the map (then) regarding to async functionality.

How you unwrap it (call .value()). Like so for example:

copy
ts
await promise.then(value => value + 1); // 6

That simple. We removed the value from the Promise container. So promises are functors, but not quite. We can also do a bit more with them.

copy
ts
promise.then(value => {
return fetch('someUrl/' + value)
}); // Promise(AjaxResult);

Now things are becoming interesting. Remember, in the case of arrays or the Identity functor:

copy
ts
const arr: number[] = [1, 2, 3];
const newArray: string[] = arr.map((x: number): string => `${x}`);

We had an array of numbers for example. In the map we get an argument in the callback that is a number and the callback returns a string. Therefore we get back an array of string. I know, I know, this is self-explanatory, but wanted to point out regardless.

The callback goes like (element: number) => string. Or in a generic form.

copy
ts
type Callback<A, B> = (element: A) => B

We simply go from A to B. The callback does not deal with the functor type. In other words we go from number to string not from number to array of string. Map will put (lift) back the string into the context of array.

It unwraps the value (from array of number to number) and automatically wraps it back (from strings to array of strings). Let's go back the our latest promise.

copy
ts
promise.then(value => {
return fetch('someUrl/' + value)
}); // Promise(AjaxResult);

That is somewhat different. The then function unwraps the value (from Promise that holds 5 to 5) but in the callback we return a new promise, so we do the lifting / wrapping / packaging ourselves.

The then callback can have two signatures

copy
ts
type Then<A, B> = (element: A) => B // we don't deal with lifting
type Then<A, B> = (element: A) => Promise<B> // we deal with the lifting

So promise is smart enough to figure out that if we return a normal value it will automatically lift it and when we return a promise it will not wrap it in an another promise.

That makes then a bit special. It can behave like map (type Callback<A, B> = (element: A) => B), or it can behave like flatMap / chain (type Callback<A, B> = (element: A) => Promise<B>).

flatMap / chain is map function where it is our responsibility to lift back our function to context. Why? We have just seen the example. To perform side effects in a composition style for example.

The best part is this: functor is a container that has a map function, while monad is a container that knows how to flatMap / chain.

That is what a monad is about. That makes Promises monad like. Not strictly, because then can behave like map, but we are more than happy with that definition.

Hope I managed to gently introduce you to functors and monads. My goal is to give you a fully fledged and detailed guide. If you are interested in more I highly recommend Dr. Frisby's guide.

It goes in a lot of details to these containers, what they are good for.

My goal was to make you aware that you are already using them, and why they are beneficial.

check out other topics