A Short Introduction to Functors

This is the first post in a series on algebraic structures. The entire series may be viewed here.

Functors show up everywhere, and are an invaluable tool when doing functional programming. They’re really quite simple, and while they come from the math world, we can understand them enough to make use of them in our JavaScript code without any other mathematical or functional programming concepts. Functors are defined in terms of types, so this article uses Flow type annotations to make the types we’re dealing with explicit.

Mapping over wrapped values

function map<A, B>(A => B, Foo<A>) : Foo<B>

Many things can be functors. The Set type is a functor, and we can define a map function on it like this:

function mapSet<A, B>(fn: A => B, s: Set<A>) : Set<B> {
const result = new Set();
for (let item of s) {
result.add(fn(item))
}
return result;
}

The Promise type is a functor, and its map function looks like this:

function mapPromise<A, B>(fn: A => B, p: Promise<A>) : Promise<B> {
return p.then(fn);
}

While we introduced functors as containers holding a value, sometimes those containers can have interesting properties. For this reason, functors are often described as “values in a context.” For Promise, that context is that the value is in the future. Set may be seen as a plain container which holds a number of values, or it may be seen as a context in which a variable may hold one of a number of possible values. (In other words, you can look at a function which returns the set { 1, 2, 3 } as something returning a container holding three results, or as something with one result, where that result might take on three possible values.) When we’re talking about values in context,map gives us a way to take functions written in a “normal” context and use them in the “special” context of a functor.

A case when functors Maybe useful

type Maybe<A> = null | Just<A>;class Just<A> {
value: A;
constructor(value) {
this.value = value;
}
}
function mapMaybe<A, B>(fn: A => B, m: Maybe<A>) : Maybe<B> {
return m === null ? null : new Just(fn(m.value));
}

Like the previous functors, Maybe holds a value in context. The context of Maybe is that the value might be null. mapMaybe lets us work with a potentially null value in a safe way, by transforming the value with a given function if the value exists, and by just returning null without applying the function if the value is null.

This comes in handy when we want to apply functions to a value which might be null. We could modify our functions to check whether their input is null, but that would lead to boilerplate being sprinkled throughout our codebase. Instead, if we want to apply some function foo to some value x which might be null, we can perform this function application using map:

foo(x); // might throw a null reference errormapMaybe(foo, x); // Will never throw

This second approach has the benefit that Flow detects that Maybe is a union type, and will force us to handle the case when our result may be null. Plus, it allows us to differentiate the case where our input x is null and the entire computation is an error (and its result is null) from the case where the input x was a good value, but foo itself returned null. (In this case, our result will be Just { value: null }.

All sorts of other things can be functors. You’ve probably seen plenty in the course of day-to-day JavaScript: arrays, trees, streams, event emitters. If you have a type which takes one generic argument, there’s a good chance you can make it a functor, with one caveat: the functor’s map function must follow the functor laws.

The functor laws

  1. map must preserve identity. That is, for any functor f, map(x => x, f) has to be equal to f. This is called “identity” because the function which returns whatever value it was called with without changing it is often referred to as “the identity function”, or id. This function is implemented as x => x.
  2. map must preserve function composition. For any functor f, and any functions foo and bar, map(foo, map(bar, f)) must be equal to map(contents => foo(bar(contents)), f).

In most cases, these laws are easy to obey as long as you’re keeping your functions pure. They are important because they let us reason about and compose functors in a consistent way, which lets us safely build abstractions on top of them. I recommend the exercise of looking at things which might be functors, and seeing whether and why these laws hold.

Composing Functors

mapFooBar<A, B>(fn: A => B, a: FooBar<A>) : FooBar<B> {
return mapFoo(x => mapBar(fn, x), a);
}

As long as mapFoo and mapBar follow the functor laws, mapFooBar will as well.

So that’s it: when we’re writing JavaScript code, functors are things holding values which we can map over in a predictable way. It’s pretty common to use them without realizing they’re there, so being on the lookout for them can help us clarify and standardize the abstractions we create.

If you want to dig deeper into functors, I also wrote a post on applicative functors, a subtype of functors with much more advanced capabilities. My series on algebraic structures goes deeper into the practical uses of functors and applicative functors when writing functional code.

Further Resources

If you want to use functors in typed JavaScript, flow-static-land provides a typed implementation of common algebraic structures in Flow, and ts-static-land provides an implementation in TypeScript. The funfix library does the same, and provides type declarations which enable it to be used from both Flow and TypeScript. Due to limitations in Flow and TypeScript’s type systems, more advanced techniques are needed to use these libraries. I’ve written about these techniques in my posts on higher-kinded types and brands.

The sample code from this post may be found here.

Thanks to Lizz Katsnelson for editing this post.

JavaScript developer with a focus on typed functional programming. He/him. https://jnkr.tech