Functors encapsulate imperative logic

A functor hides some piece of common imperative code (and makes it composable)

Imagine we have a small piece of code that, given a number multiplies it by 5 and adds 3 to the result. We can write this in the imperative style using two tiny helper functions

1
2
3
4
5
// compute 2 * 5 + 3
const add3 = x => x + 3
const mul5 = x => x * 5
console.log(add3(mul5(2)))
// 13

The expression console.log(add3(mul5(2))) is a nested call of 3 functions: first the mul5 is called, followed by add3, finally followed by console.log. In each case, the result of the inner function call is passed to the outer function. We can see this clearly by being more explicit in our code

1
2
3
4
5
const initial = 2
const x = mul5(initial)
const y = add3(x)
console.log(y)
// 13

As a personal preference, I prefer reading functional calls top to bottom to reading them inside out (equivalent to right to left).

The combined expression console.log(add3(mul5(2))) is called "composition"

  • the nested function calls are composed together. The later multi-line code is not a composition, and has a disadvantage - the variables are all shared in the same namespace, making the code brittle. Accidental variable overwrite is always dangerous.

Can we combine the advantage of the compositional approach (no stray variables) with the clarity of the imperative approach? Yes, by wrapping the common task of the multi-line imperative approach in a piece of utility code - a simple "box" functor.

Notice the common pattern in each line. The imperative code does several things over and over:

  1. receives the result of a function call with a single argument
  2. stores the result in a variable
  3. calls next unary function passing the variable as an argument
  4. goes to step 1

In JavaScript the parts we want to automate look like these (commenting out everything else)

1
2
3
const privateVariable = ...
... = fn(privateVariable)
// the pattern repeats

Box functor

Here is a simple utility function that hides a given value to avoid accidental name clashes. The returned "box" automates the above "call function - store result - call next function" algorithm. The only method the returned "box" object has is .map. The map is used to execute a given unary function.

1
2
3
const Box = _ => ({
map: f => Box(f(_))
})

Note the result is stored "back" in a returned "box", thus the process can continue. The composition is thus replaced with calling the box's .map function repeatedly.

1
2
3
4
5
Box(2)
.map(mul5)
.map(add3)
.map(console.log)
// 13

Hmm, isn't the above "Box" an example of Object Oriented Programming (OOP)? No. We capture the private variable using closure, and we never use "this" or "new" keywords.

If we need to grab the synchronous value from the "box", we can add a function to return the value from the closure, avoiding the awkward .map(console.log) call. This allows us to connect the functor "box" to the rest of the imperative code.

1
2
3
4
5
6
7
8
9
10
11
const Box = _ => ({
map: f => Box(f(_)),
fold: () => _
})
console.log(
Box(2)
.map(mul5)
.map(add3)
.fold()
)
// 13

Note that box.fold() call is a terminal method - one cannot attach any more .map() calls after the .fold(), because the wrapped value is returned. We can still continue mapping over the original "box" though.

1
2
3
4
5
6
const box = Box(2)
.map(mul5)
console.log(box.fold())
// 10
console.log(box.map(add3).fold())
// 13

Sometimes it is useful to just print the value for debugging, which we can do by adding .inspect method to the "box"

1
2
3
4
5
6
7
8
9
10
11
const Box = _ => ({
map: f => Box(f(_)),
fold: () => _,
inspect: () => `Box(${_})`
})
console.log(
Box(2)
.map(mul5)
.map(add3)
)
// Box(13)

Maybe functor and composability

The "box" just implements simple common imperative logic

1
2
3
const privateVariable = ...
... = fn(privateVariable)
// the pattern repeats

One can place more logic inside the .map() method, for example, we can put the "truthy" condition on the value inside, creating "maybe" functor.

1
2
3
4
5
const privateVariable = ...
if (privateVariable != null) {
... = fn(privateVariable)
// the pattern repeats
}

What happens if the condition is false? Just like in a regular if/else tree we skip all the branches. Look at the binary tree formed by nested "success" paths

1
2
3
4
5
6
7
8
9
10
if (condition1) {
if (condition2) {
if (condition3) {
f() // success 3 times
}
// failed condition 3
}
// failed condition 2
}
// failed condition 1

In all cases, the failure is the same, and should skip any other evaluation. To implement we need special "placeholder" type we can return that will skip all subsequent .map(fn) calls. In my code let us call it "None".

1
2
3
4
5
6
7
8
9
10
const None = () => ({
map: _ => None(),
fold: () => null,
inspect: () => `None`
})
const Maybe = _ => ({
map: f => _ == null ? None() : Maybe(f(_)),
fold: () => _,
inspect: () => `Maybe(${_})`
})

Note that we can transition from Maybe to None but not in the opposite direction. Once the condition is false, everything else is skipped.

The above code makes it simple to write code that is safe against undefined or null values.

1
2
3
4
5
6
7
8
9
10
11
12
console.log(
Maybe()
.map(mul5)
.map(add3)
)
// None
console.log(
Maybe(2)
.map(mul5)
.map(add3)
)
// Maybe(13)

The above code is also composable. Ordinarily, we cannot execute more statements inside "if/else" branches. For example, if we execute the given function "foo" where f and g are functions, and want to execute function h as well, we have to call the condition again outside foo ourselves.

1
2
3
4
5
6
7
8
9
10
11
function foo(value) {
if (value) {
f()
g()
}
}
var x = ...
foo(x)
if (x) {
h()
}

Using Maybe, we can just return the functor and compose call to function h by adding .map(h) to the returned functor!

1
2
3
4
5
6
7
8
function foo(value) {
return Maybe(value)
.map(f)
.map(g)
}
var x = ...
foo(x)
.map(h)

That is why functors are wrappers that allow us to compose functions.

Related

Update 1

A sharp-eyed reader Irakli Safareli @safareli has noted that the above functor Maybe is not really a functor because it does not respect the following composition law (that every Functor should obey)

1
2
// for any initial value and functions f and g
Functor(initial).map(x => f(g(x))) === Functor(initial).map(g).map(f)

For example in this case the results are different

1
2
3
4
5
6
const g = _ => null
const f = _ => 1
console.log(Maybe(10).map(x => f(g(x))))
console.log(Maybe(10).map(g).map(f))
// Maybe(10)
// None

Hmm, what is going on? The problems is that f(g(x)) expression does not follow what we implemented in the above Maybe. The above Maybe tries to mimic the success path were every input argument is checked before calling the next function, and not just the initial value.

1
2
3
4
5
6
7
8
const first = ...
if (first) {
const second = g(second)
if (seconds) {
const third = f(second)
// etc
}
}

In our case the predicate evaluation happens on every .map(), but the Functor should NOT make such decisions if it wants to just compose functions. This would be equivalent to making a decision only when constructing the functor instance initially.

1
2
3
4
5
const first = ...
if (first) {
const t0 = g(first)
const t1 = f(t0)
}

If we really wanted to implement the conditional logic at each step, functions f and g could return an instance of Maybe (that implements the predicate logic at creation).

Of course then we should implement the additional logic to deal with functions that return wrapped results. And those would be Monads with their .chain method :)