What the Functor?
A week ago I wrote an article on implementing Maybes in JavaScript, and while it was generally well received, it did draw some (well justified) criticism from some commenters. Specifically, there are some well-defined mathematical rules that my Maybe didn't obey. Terminology and nomenclature are always important if we want to communicate effectively with others, so although my Maybe has some utility, I wanted to learn some more about these mathematical rules so that:
- Code I write is interoperable with existing functional libraries
- I can understand the writing of others
- My writing is understandable to others
- It's interesting and fun!
One of the recommendations I received was to look at the Fantasy Land Specification which defines a set of algebraic data types. This a very technical specification and I'm not afraid to say parts of it go over my head at the moment, so I started with the most basic types. One of the simplest, and yet seemingly most commonly used and well known, is the Functor.
If a type is to become a functor, it need only do one thing: it must have a map
method. This method has one argument (arity-1), which must be a function, and must return another functor of the same type. I use TypeScript a lot, and so we can define an interface for a functor in that language:
interface Functor<T> {
map: <U>(fn: (a: T) => U) => Functor<U>;
}
We can now make concrete implementations of data structures that implement this interface, and be confident that if type-checking passes then we are obeying the rules. We'll start with a simple type that will wrap a number, and give us a simple way to inspect the value:
class NumberFunctor implements Functor<number> {
private value: number;
constructor(value: number) {
this.value = value;
}
inspect() {
console.log(this.value);
return this;
}
}
We now need to implement a map method - this method will take a function, pass it the internal value we are holding, and return a new instance of NumberFunctor:
class NumberFunctor implements Functor<number> {
private value: number;
constructor(value: number) {
this.value = value;
}
inspect() {
console.log(this.value);
return this;
}
map(fn) {
return new NumberFunctor(fn(this.value));
}
}
It can be used like this:
const twenty = new NumberFunctor(20);
twenty.map(x => x + 1).inspect(); // 21
twenty.map(x => x * 2).inspect(); // 40
Because we are returning a new functor each time, we can also chain map calls together:
const fifty = new NumberFunctor(50);
fifty
.map(x => x * 2)
.inspect() // 100
.map(Math.sqrt)
.inspect(); // 10
The key point is that when a type implements Functor, it means that it has a way of transforming the value(s) that it contains. When we use the map
method we don't have to care about how this transformation occurs - that is the internal responsibility of the specific functor.
To illustrate this further let's make a very simple Tuple functor that will hold two values - a first and a second:
class Tuple implements Functor<number> {
private first: number;
private second: number;
constructor(first: number, constructor: number) {
this.first = first;
this.second = second;
}
inspect() {
console.log(`Tuple: (${this.first}, ${this.second})`);
return this;
}
}
How are we going to implement map
for our tuples? We're going to apply the provided function to each of the values we hold, and return a new tuple with the results:
class Tuple implements Functor<number> {
private first: number;
private second: number;
constructor(first: number, constructor: number) {
this.first = first;
this.second = second;
}
inspect() {
console.log(`Tuple: (${this.first}, ${this.second})`);
}
map(fn) {
return new Tuple(
fn(this.first),
fn(this.second)
);
}
}
We can use it just like the NumberFunctor:
const tuple = new Tuple(49, 100);
tuple.map(x => x * 2).inspect(); // Tuple (98, 200)
tuple.map(Math.sqrt).inspect(); // Tuple (7, 10);
Hopefully at this point you are thinking about where we have seen a map
method before, and perhaps you realised you have used Array#map
:
[1,2,3].map(x => x * 2); // [2,4,6]
It's easy to see now that JavaScript arrays implement the Functor interface, and they implement a map
method that applies the function to each of the values they hold, and return a new array.
Thanks to everyone who read and commented on the original Maybe article, especially to those who wrote insightful and constructive comments.