Simple Explanation of Applicative Functor

Introduction

Starting from the concept of Functor, we can combine functions by wrapping values in containers as follows:

const addThree = (x) => x + 3
const multiplyByTwo = (x) => x * 2
Maybe(2)
.map(addThree); // Maybe(5)
.map(multiplyByTwo); // Maybe(10)

But what if the function itself is also wrapped in a container?

const maybeAddThree = Maybe(addThree)

The map of Functor can only operate on values and cannot act on “values within a container,” which is why we need the Applicative Functor.

What is an Applicative Functor?

It is a structure that has more functionality than Functor, which not only satisfies Functor’s capabilities but also allows “functions in the box to apply to values in another box.”

function Maybe(value) {
return value == null ? Nothing() : Just(value);
}
function Just(value) {
return {
map: f => Maybe(f(value)),
apply: f => f.map(value),
getOrElse: () => value,
};
}
function Nothing() {
return {
map: () => Nothing(),
apply: () => Nothing(),
getOrElse: defaultValue => defaultValue,
};
}
const add = a => b => a + b;
Maybe(add)
.apply(Maybe(1)) // Maybe(1 + b)
.apply(Maybe(1)) // Maybe(1 + 1)
.getOrElse(0) // 2

The above Applicative Functor achieves computation without unwrapping the contents.

I Still Don’t Understand the Use of Applicative Functor

Suppose we want to validate a user’s “input name” and “Email” and convert them to lowercase and uppercase; each validation may fail. Using Functor, it would be written like this:

const validateName = name =>
name ? Just(name) : Nothing();
const validateEmail = email =>
email.includes('@') ? Just(email) : Nothing();
const nameResult = validateName("webdong")
.map(name => name.toUpperCase())
.getOrElse("Invalid name")
const emailResult = validateEmail("[email protected]")
.map(email => email.toLowerCase())
.getOrElse("Invalid email")
const makeUser = name => email => ({ name, email });
console.log(makeUser(nameResult)(emailResult))

Handling multiple Functors can be problematic, or dealing with other Functors inside a Functor can also pose challenges:

// Just(Just({ name, email }))
validateName("Rice")
.map(name =>
validateEmail("[email protected]")
.map(email => makeUser(name)(email))
);

With Applicative Functor, we can easily combine multiple Functors:

const validateName = name =>
name ? Maybe(name) : Nothing();
const validateEmail = email =>
email.includes('@') ? Maybe(email) : Nothing();
const makeUser = name => email => ({ name, email });
Maybe(makeUser)
.apply(validateName("webdong"))
.apply(validateEmail("[email protected]"))
// → Maybe({ name: "Rice", email: "[email protected]" })
Maybe(makeUser)
.apply(validateName(""))
.apply(validateEmail("[email protected]"))
// → Nothing

Alternatively, we can view Promise as an Applicative Functor:

Promise.resolve(add)
.then(f => Promise.all([Promise.resolve(2), Promise.resolve(3)])
.then(([a, b]) => f(a)(b)))
.then(console.log); // 5

Definition of Applicative Functor

Applicative Functor can be seen as an advanced version of Functor. Functor can only “apply functions to values within a container,” while Applicative further allows “functions inside a container” to operate on “values in another container.” To implement Applicative, two methods need to be practiced:

  • of: Wraps a regular value into a container.
  • apply or ap: Allows “functions in the container” to operate on “values in the container.”
  1. Identity
// Wrapping with a "content-preserving" function and applying it, the result should be the same as the original value.
// For example: Maybe.of(x => x).apply(Maybe(5)) === Maybe(5)
A.of(x => x).apply(v) === v
  1. Homomorphism
// Applying the function directly and wrapping both the function and the value before applying should yield the same result.
// For example: Maybe.of(f).apply(Maybe.of(x)) === Maybe.of(f(x))
A.of(f).apply(A.of(x)) === A.of(f(x))
  1. Interchange
// The order of application can be interchanged, as long as the logic of function application remains the same.
// For example: Maybe(fn).apply(Maybe.of(y)) === Maybe.of(f => f(y)).apply(Maybe(fn))
u.apply(A.of(y)) === A.of(f => f(y)).apply(u)
  1. Composition
// When applying multiple functions, it should yield the same result as their composition.
// For example: Maybe.of(compose).apply(u).apply(v).apply(w)
// is equivalent to: u.apply(v.apply(w))
A.of(compose).apply(u).apply(v).apply(w) === u.apply(v.apply(w))