Simple Explanation of Functor

A Simple and Clear Explanation of Functor

Introduction

I’m not a mathematician and I’m not very interested in category theory🔗, but it profoundly relates to advanced concepts of Functional Programming. Through practical applications, we understand how these theories can help write better-maintained programs.

Functor

Think of it as a “box that holds values”, a “mappable structure that obeys specific rules” allowing operations on its contents while retaining the box’s structure. For example: 2 + 3 = 5, but what if the number is “wrapped”? For example:

  • [2] (in an array)
  • Promise.resolve(2) (in asynchronous computations)
  • Maybe(2) (may have a value, or may not)

At this point, you can’t directly modify 2, but Functor provides a way to operate on values within the container, usually called map.

  • [2].map(x => x + 3); // [5]
  • Promise.resolve(2).then(x => x + 3); // Promise(5)
  • Maybe(2).map(x => x + 3); // Maybe(5)

Why Do We Need Functor?

Wrapping values in a “box” is to give values context. For example, creating a Maybe Functor to represent “a value that may exist and may not exist”, returning different “boxes” based on input according to the rules defined inside the “box”.

  • If there is a value, calculate normally.
  • If there is no value, skip the computation and return Nothing.
function Maybe(value) {
return value == null ? Nothing() : Just(value);
}
function Just(value) {
return {
map: fn => Just(fn(value)),
getOrElse: () => value,
};
}
function Nothing() {
return {
map: () => Nothing(),
getOrElse: defaultValue => defaultValue,
};
}
// Compared to failing to get Nothing, using `getOrElse` returns the value or provides a default.
const a = Maybe(2).map(x => x + 3).getOrElse(0);
const b = Maybe(null).map(x => x + 3).getOrElse(0);
console.log(a, b) // 5, 0

The above example of the Maybe Functor avoids the need to repeatedly write conditions for values with no value by wrapping them in a context:

// Continuously checking the value
function getUserEmail(userId) {
const user = findUser(userId);
if (user !== null && user !== undefined) {
const profile = user.profile;
if (profile !== null && profile !== undefined) {
const email = profile.email;
if (email !== null && email !== undefined) {
return email.toLowerCase();
}
}
}
}
// Values wrapped in Functor context
function getUserEmail(userId) {
return Maybe(findUser(userId))
.map(user => user.profile)
.map(profile => profile.email)
.map(email => email.toLowerCase())
.getOrElse('[email protected]');
}

Functor Definition

  • Identity

    // Change its contents without changing its structure.
    // For example: [1, 2, 3].map(x => x) is the same as [1, 2, 3].
    Functor.map(x => x) === Functor
  • Composition

    // Sequentially mapping two functions should be equivalent to mapping their composed function.
    // For example: [1, 2, 3].map(x => f(g(x))) is the same as [1, 2, 3].map(g).map(f).
    const f = (x) => x * 2
    const g = (x) => x + 1
    Functor.map(x => f(g(x))) === Functor.map(g).map(f)

Summary

The concept of the “box” in programming languages is formally referred to as: “type constructor,” used to accept a type as a parameter to generate a new type.
type Maybe<T> = Just<T> | Nothing
  • A Functor is a structure with a map method that allows transforming values while keeping the structure unchanged.
  • Functors allow functions to compose based on containers without worrying about the actual context in which the values exist, meaning they can be used to encapsulate side effects or contexts.

Further Reading