What’s NOT a functor?

Thai Pangsakulyanont
4 min readDec 16, 2015

That’s a good question! In this response, I’ll show some cases where something looks like a functor (it has a .map(f) method), but did not obey every functor laws.

(Again, I’m going to assume a definition of functor as specified by Haskell and the Fantasy Land specification.)

The ValueMapper

Let’s look at this ValueMapper class.

class ValueMapper {
constructor (object) {
this.object = object
}
map (f) {
const mapped = { }
for (const key of Object.keys(this.object)) {
mapped[key] = f(this.object[key])
}
return mapped
}
}

This is not a functor, because the .map(f) method did not return a wrapper. Although it correctly returns the mapped value, true, but it’s not properly wrapped.

This makes it not chainable, which also violates the second law of functor.

const myData = { myAge: 22, friendAge: 21 }
const myDataMapper = new ValueMapper(myData)
myDataMapper.map(addThree).map(addThree)
// =! myDataMapper.map(...).map is not a function

Ruby Hash

In Ruby, an array is a functor, but a hash is not (even though they both provide .map(&f) method).

Let’s define our identity function:

id = -> x { x }

Next, let’s create a hash:

myData = { myAge: 22, friendAge: 21 }

Now, let’s map the identity function over it:

myData.map(&id)
# => [[:myAge, 22], [:friendAge, 21]]

Here, a nested array is returned instead of the same hash, and they are not considered equivalent:

myData.map(&id) == myData
# => false

And thus, a Ruby hash is not a functor.

Maybe

In Chapter 8 of Professor Frisby’s Mostly Adequate Guide to Functional Programming, the author presented a Maybe functor, which looks like this (rewritten in ES6).

class Maybe {
constructor (value) {
this.__value = value
}
static of (value) {
return new this(value)
}
static nothing () {
return new this(null)
}
isNothing () {
return this.__value == null
}
map (f) {
return (this.isNothing()
? Maybe.nothing()
: Maybe.of(f(this.__value))
)
}
toString () {
return (this.isNothing()
? 'Maybe.nothing()'
: `Maybe.of(${this.__value})`
)
}
}

This Maybe is used to represent two possible states:

  • When there is something (e.g. Maybe.of(42))
  • When there is nothing (e.g. Maybe.nothing())

Now, Maybe.of(null) and Maybe.of(undefined) is considered as Maybe.nothing() (but for good reasons which we will see).

Unfortunately, this Maybe class violates the second functor law, let’s take this data as an example.

const me = { firstName: "Thai" }

I want to get the length of my first name. Living by the Single Responsibility Principle, I create two functions to do it.

const firstNameOf = (person) => person.firstName
const lengthOf = (string) => string.length

Let’s try it by performing two mappings first:

console.log(Maybe.of(me).map(firstNameOf).map(lengthOf))
// => Maybe.of(4)

Now let’s try it by composing these two functions first, then perform the mapping:

const firstNameLengthOf = (person) => (
lengthOf(firstNameOf(person))
)
console.log(Maybe.of(me).map(firstNameLengthOf))
// => Maybe.of(4)

We get the same thing. This doesn’t seem like a violation of the second functor law…

Hmm…

How about trying it with a different object? Let’s see…

const myData = { myAge: 22, friendAge: 21 }

Let’s first perform two mappings:

console.log(Maybe.of(myData).map(firstNameOf).map(lengthOf))
// => Maybe.nothing()

In case you are wondering why Maybe.nothing() is returned, here’s why: First, I wrapped myData in a Maybe.

console.log(Maybe.of(myData))
// => Maybe.of({ myAge: 22, friendAge: 21 })

Then I take the first name. Since myData does not have the firstName property, I get undefined back, which turns into a Maybe.nothing():

console.log(Maybe.of(myData).map(firstNameOf))
// => Maybe.nothing()

Now, since we now have nothing, there is virtually nothing to map. Hence, we simply get Maybe.nothing() back.

console.log(Maybe.of(myData).map(firstNameOf).map(lengthOf))
// => Maybe.nothing()

I hope you can see how Maybe can be useful: When a maybe is mapped into null or undefined, it turns into a nothing. It will prevent the lengthOf function to run on undefined value, which can prevent our code from throwing errors. This is a very useful concept, but still, it’s not a functor in the purest sense.

To see why, we’ll map the composed function over that maybe. If Maybe is a functor, then we should also get Maybe.nothing() back…

console.log(Maybe.of(myData).map(firstNameLengthOf))
// ! Cannot read property 'length' of undefined

It throws an error! The second law does not hold anymore, and thus, this Maybe is not a functor.

This happens because Maybe.of(null) and Maybe.of(undefined) has automagically turned into Maybe.nothing(). The shape has changed.

Although the same type, the shape turned from “something” into “nothing.”

In Haskell, a Maybe can hold any value, even () which represents a lack of any value, and it would still consider that ‘something.’

Prelude> Just ()
Just ()

If we make distinction between these three — namely Maybe.of(null), Maybe.of(undefined) and Maybe.nothing() — then this Maybe class wouldn’t be as useful because you wouldn’t be able to chain .map(f) calls and make it not throw an error in case something is mapped to null or undefined along the way.

It’s mappable, but it’s not a real functor.

Still, it works like that for your convenience. We just need to compromise to make up for that billion dollar mistake.

--

--

Thai Pangsakulyanont

(@dtinth) A software engineer and frontend enthusiast. A Christian. A JavaScript musician. A Ruby and Vim lover.