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
A functor is a data structure which acts like a container holding a generic type. There must be a map
function defined for that data structure, which applies a function to the contents of that functor. For some generic class Foo<A>
to be a functor, the signature of map
must be:
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
Maybe
is a commonly used functor in functional programming, whose context provides a practical benefit:
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
For something to be a functor, its map
function has to obey two laws.
map
must preserve identity. That is, for any functorf
,map(x => x, f)
has to be equal tof
. 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”, orid
. This function is implemented asx => x
.map
must preserve function composition. For any functorf
, and any functionsfoo
andbar
,map(foo, map(bar, f))
must be equal tomap(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
An important property of functors is that the composition of two functors is also a functor. That is, given a functor Foo<A>
and a functor Bar<A>
, we can define the type type FooBar<A> = Foo<Bar<A>>
, and define the mapFooBar
function as so:
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
Functors are one of many abstractions from the functional programming world which are defined as a generic data type following certain laws. Such abstractions are often called “algebraic structures”. The most common way to work with these structures in JavaScript is by using libraries which conform to the Fantasy Land specification, which details the interface which these structures should have. In Fantasy Land, functors are created by extending or wrapping data types such that the map
function exists on the prototype of the type being used, like array.map
, promise.map
, maybe.map
, etc. Alternatively, the Static Land specification defines a style like that used in this post, where functions are declared separately from the types they operate on. I prefer the static style, but the practical differences between the two are small, and the benefits that can be gained by using either are large.
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.