In category theory, functors are mappings between categories. I.e., functors are "containers" that lifts objects from one category to another. Here, I will present how to understand this concept in terms of types and functions.
What is considered a functor?
To be considered a functor, a mapping between two categories must preserve identity and composition.
So let's imagine two categories C and D and a functor F that maps objects from C to D.
Identity is a unique morphism that connects one object to itself. This morphism is useful because if you compose this morphism with any other morphism f, you will still have f. I.e., these three functions (our morphisms) will behave in the same way:
(x) => f(id(x))
(x) => id(f(x))
(x) => f(x)
Preserve identity means that, for every object x in C, mapping F(x) with the identity morphism is the same as applying the identity morphism to F(x). I.e., these two expressions must return the same thing:
F(x).map(id)
id(F(x))
And preserve composition means that mapping with a composition of functions in a functor will have the same behavior as mapping with the two functions separately. I.e., these two expressions must return the same thing:
F(x).map((x) => f(g(x)))
F(x).map(g).map(f)
Types as objects and endofunctors
Knowing then what functors are, we can proceed to see how they can be applied in programming. In fact, the application is much simpler to understand than the theory. That's because in a programming environment, the type system will be considered our category, and all the functors will be just endofunctors.
And what is an endofunctor? It is basically a functor that maps objects from one category to the same category. In our case, all the functors will map values from one type (object) to another type (still in the same category).
So let's see a simple example of the application of functors in TypeScript.
The Maybe functor in TypeScript
As we saw, functor must be a mapping. So we have to define an interface that requires an implementation of a map function:
type FunctorMap<A, B> = (a: A) => B;
interface Functor<A> {
map<B>(f: FunctorMap<A, B>): Functor<B>;
}
With this interface, it will be simple to implement the Maybe functor. In short, Maybe is a type that is able to contain an empty value (Nothing) or a value of a respective type (Just).
But to implement the map function, we have to ask ourselves: the Maybe type is a functor? To answer this, we just have to check if it respects the properties.
First, every value of type Maybe can be constructed two ways: Maybe.nothing() or Maybe.just(<value>). So,
// "Maybe" preserves identity
Maybe.just(x).map(id)
Maybe.just(x)
id(Maybe.just(x))
// And preservers composition
Maybe.just(x).map(g).map(f)
Maybe.just(x).map((x) => f(g(x)))
And what about the Maybe.nothing() constructor? In this case, all maps will simply skip the application of the function, and return another Maybe.nothing().
With this, we have demonstrated that the Maybe type is a functor. Let's finish to implement it now:
class Maybe<A> implements Functor<A> {
constructor(private value: A | null) {}
static just = <A>(value: A) => new Maybe<A>(value);
static nothing = <A>() => new Maybe<A>(null);
map<B>(f: FunctorMap<A, B>): Maybe<B> {
if (this.value === null) return Maybe.nothing();
return Maybe.just(f(this.value!));
}
}
With this implementation, we can test how it will perform with a basic example:
Maybe.just<number>(1)
.map((x) => x + 1)
.map((x) => x * 2) // Just(4)
Maybe.nothing<number>
.map((x) => x + 1)
.map((x) => x * 2) // Nothing
Conclusion
As you see, functors are very simple, especially if you see them as containers that implements a map function. If you look closely, this is very similar to what we do in JavaScript with arrays and promises, with just a bit more of formality :)
Top comments (0)