Back to business…

Functor non-examples with arrays

How about trying to apply a function f only to the last array entry:

[1, 2, 3].map(f) = [1, 2, f(3)]

On the surface, both Functor laws apply! The identity f is lifted to the identity map(f) and composition to composition. Yet it is not a functor.

The problem arises when f changes the type. Like isEven(x) that returns boolean. We would get

[1, 2, 3].map(isEven) = [1, 2, false]

that is no more array of booleans. That is, we won’t get

map(isEven): [integer] -> [boolean]

as would be required from a Functor.

How about applying map(f) only to the first array entry and forgetting the rest:

[1, 2, 3].map(f) = [f(1)]

Now with f = isEven , our map(isEven) will send array of integers into array of booleans as desired. And it will even lift compositions to compositions as in the second law.

But we run into problem with the first law saying that the identity should be lifted to the identity:

[1, 2, 3].map(id) = [1]

vs [1, 2, 3] that we need to be returned by the identity

So again, not a Functor.

A Functor is NOT “simply a container”

A container needs to hold something. One thing or several? If the array [1,2,3] were “simply a container” holding 1,2,3 , why is it different from the array [3,2,1] holding exactly the same things?

So no, it is more than that. Some additional structure is needed. Like assigning places to each elements. Or a graph or tree would need to specify all the edges between the elements, in additional to “simply holding” them.

Having said that, simply wrapping anything into container is an important and useful example of a Functor. Which can well provide some intuition. But it is an example! And examples are useful but can also deceive into wrong intuition. Like something only specific to that example, that can be confused for a general feature. For instance, that mentioned example is known as the so-called Identity Functor. Which is a boring thing in maths but more useful in programming. In JavaScript, the wrapper map

Identity = x => ({

map: f => Identity(f(x))

})

is actually NOT the Identity Functor. It is a thing in addition to the Functor, making the Identity the so-called Pointed Functor. Which is very useful, as it lets us write

Identity(x).map(f).map(g).map(h)

instead of

h(g(f(x)))

And does provide some intuition. But it is still an example.

The functor “map” does NOT “simply run the function over all values (or elements)”

Sometimes, it is only some of the values. The classic example is the Pair Functor:

To every type (set of values) a , it associates the set of Pairs (x, y) where y is of type a , and x is anything. (Note here how don’t associate anything to the individual values, only to their sets.)

Now define the lift map(f) by only applying f to the second entry:

(x, y).map(f) = (x, f(y))

Yes — the function f only applies to the second argument, this is not a typo! Unlike the Array Functor, where the type is the same for all entries, the Pair Functor does not impose any restriction on the first element of the pair.

So no, the functor map does not always run over all values inside the container. That looks weird first, but becomes less “unnatural” when comparing to the JavaScript comma operator that always picks the second entry:

const result = (x = 2, x * x) //=> 4

And we get map(f) going from F(a) to F(b) whenever f: a -> b , as required from a Functor. Because this time, the type of the first entry does not matter!

Both laws apply too and we get a Functor. Note how it is similar to the above Array non-example, where the map looks exactly the same. The main difference here is in F(a) , the associated set of values provided by the Functor. In the array case, we consider arrays with all entries of type a , in the case of pairs only the second entry is of type a . Thus, in the array case, the same map does not return a correct type.

Some further reading on Pairs for JavaScript lovers: http://www.tomharding.me/2017/04/27/pairs-as-functors/

The Promise non-example

Yes, the native JavaScript Promise is not a Functor (nor is a Monad). Even though it might look like one, namely Promise(a) looks like the set of all promises holding values of type a .

Yet in fact it isn’t. For instance, if a is the set of all objects, what is Promise(a) ? All promises holding object values? But what about “thenable” objects such as obj={then: x => x} ? These can never be held by promises because the promise constructor will keeping calling .then whenever it is a function and return the final value rather than the object itself. So what is Promise(object) then?

As we have no knowledge about the type returned after recursively calling all "then” methods by the promise, we have to declare “ Promise(object)” to be the set of promises holding all possible types!

Ok, so Promise(object) is the set of all promises. And so is Promise(a) whenever a may include “theneables”. Is this a problem? Suppose we have a function f from objects to objects. A Functor would need to provide a map operator so that map(f) is a function from Promise(object) to Promise(object) . That is, map(f) should be applicable to any promise holding any value, not just objects! So suddenly we need to lift functions acting only on objects to promises holding arbitrary values? For instance, f may compose functions stored as p and q props and store as r prop:

const f = obj => {r: x => obj.p(obj.q(x))}

To make it work, we declare our type a to be the set of objects obj with obj.p and obj.q being functions. But since we cannot exclude obj from being “theneable”, we must include all promises into Promise(a) , and then we run into the problem defining map(f) for all promises holding arbitrary values. Because how are we supposed to compose .p and .q props and store as .r on arbitrary values that are not even objects?

Many ways to skin a cat, sorry, a Functor

As we have just seen with arrays, there may be several legal ways to define the map . If you were not impressed by that example, think of some fancy graphs or trees, where there different rules how to apply you function to different values.

Conclusion

I hope this tiny non-exhaustive excursion into the Functor non-examples helped to dispel some myths aka confusions about Functors. And if not, don’t give up. Find the best explanation for your taste. The one with similar goals. A mathematically rigorous introduction may not be the same as the one explaining how the things are “useful”.

And finally, dear author, please don’t hide the “scary details” from your readers. Not everyone needs everything completely, strictly and mathematically defined. What your reader will appreciate, however, is to be told where is a metaphor, where is an example, where is a simplification, and where is the real thing.

PS. Critics, corrections, objections, improvements and even disprovements are welcome :)