When you’re digging deep into a language or a paradigm, you’ll inevitably find some concepts that are hard to understand.
Not every developer have an academic background so same concepts, terms or abstractions are hard to understand and adopt.
One concept that’s certainly hard to understand is: what is a Functor?
Well, I bet you’re already using functors every day without knowing it!
A Functor is a map between two categories.
Real world example? The awesome .map method from our first article, can be applied on arrays… which are functors!
As written in the Haskell Data.Functor specification, Functors uniforms action over a parameterized type, generalizing the map function on lists.
Let’s throw away all that abstract things and let’s see in practice how to adopt Functors!
const arr = [1, 2, 3, 4, 5];
const doubleArr = arr.map((x) => x + x);
As you can see in the example above, an array is functor because we can map over it.
We can apply a function to each element and get back the same structure, respecting the Functor Laws:
Functors must preserve identity morphisms When performing the mapping operation, if the values in the functor are mapped to themselves, the result will be an unmodified functor.
Functors preserve composition of morphisms If two sequential mapping operations are performed one after the other using two functions, the result should be the same as a single mapping operation with one function that is equivalent to applying the first function to the result of the second.
const functor = [0, 10, 20, 30];
const double = (x) => x + x;
const add5 = (x) => x + 5;
functor.map((x) => double(add5(x)));
// Is equivalent to
functor.map(double).map(add5(x));
So as you can see, we can applydouble and add5 functions to our array using the map method. Chaining multiple map methods will always produce the same output, thanks to the Functor Laws!
Now you may ask: “as far as I know, .map can be used only on arrays, so only arrays are functors!” …wrong! You can create a Functor out of mostly any value, that’s why is so important to understand what Functors are!
Let’s implement a Functor from zero, respecting the Functor Laws:
class Functor {
constructor(val) {
this.val = val
}
}
Great, we just defined an algebra that will be our starting point!
Now, in order to make it look like a real Functor, we need to make it mappable, always respecting the Functor Laws.
class Functor {
constructor(val) {
this.val = val
}
map(fn) {
return new Functor(fn(this.val));
}
}
Great! We just created our first mappable Functor! Let’s test it:
const myString = new Functor("I feel happy.");
myString
.map(x => x.toUpperCase())
.map(x => x.replace(".", "!"))
.map(x => x + " what about you?")
// => Functor { val: "I FEEL HAPPY! what about you?" }
That’s awesome! Let’s see if it respects the Functor Laws:
Object can be Functors too! And it’s easiest than you think:
class Functor {
constructor(val) {
this.obj = obj
}
map(fn) {
return new Functor(
Object.keys(this.obj)
.map(x => { return {[x]: fn(this.obj[x])} })
);
}
}
Ok, that may not be so easy if you’re not familiar with immutability. So, I’ll rewrite the same function using loops to help you understand:
class Functor {
constructor(val) {
this.obj = obj
}
map(fn) {
let mapped = {};
for (const key in this.obj) {
mapped[key] = fn(this.obj[key]);
}
return new Functor(mapped);
}
}
Great! So now that we have our Functor, let’s try it out!
const myObj = new Functor({ a: 1, b: 2, c: 3 });
myObj
.map(x => x + 2)
.map(x => x * 10);
// => Functor {obj: {a: 30, b: 40, c: 50}}
Isn’t that awesome?
As you may know, classes in JavaScript are just functions. Given the fact that classes contains an internal state, they are not pure functions.
So is there a way to build a “pure” Functor?
const Functor = (x) => ({
map: (fn) => Functor(fn(x))
});
Holy guacamole that’s sooooo easy! Let’s test it against a number!
const log = (x) => {
console.log(x);
return x;
}
const double = (x) => x + x;
const add5 = (x) => x + 5;
const myNumber = Functor(10);
myNumber
.map(log) // => 10
.map(double)
.map(log) // => 20
.map(add5)
.map(log); // => 25
Amazing! As you may already know, the following expressions are equivalent:
myNumber.map(log).map(double).map(log).map(add5).map(log);
myNumber.map((x) => log(add5(log(double(log(x))))));
Functors are a key topic in both mathematics and computer science.
Without understanding Functors, we won’t be able to understand Monads and other amazing things.
Functors adds some kind of safety in our program, just think how predictable our output can be.
They are also an awesome abstraction which enables you to work on a certain way on any datatype. Remember undefined and null? Functors makes them safe:
const hasValue = (x) => (typeof x !== "undefined" || x !== null);
const ifHasValue = (x) => ({
map: (fn) => hasValue(x) ? x.map(fn) : x
});
const double = (x) => x + x;
const add5 = (x) => x + 5;
ifHasValue(Functor(undefined))
.map(double)
.map(add5);
ifHasValue(Functor(10))
.map(double)
.map(add5);
So as you can read, we can only apply certain functions when a value is known (so not undefined or null).
So, in few words, a Functor is a thing we can map over. If you’re familiar with **category theory**, you may have noticed that a Functor maps from category to category.
A Functor which maps from category back to the same category is called Endofunctor (for instance, a monad is an endofunctor).
I really hope that Functors aren’t a mystery anymore!
Did you like this article? Consider becoming a Patron!