Fantas, Eel, and Specification 6: Functor

Fantasy Landers, assemble! We’ve been concatenating for two weeks now; are you ready for something a bit different? Well, good news! If you’re humming, “Oh won’t you take me… to functor town?”, then this is the article for you. Today, friends, we’re going to talk about functors.

Yes, so, at the very end of last year, I wrote an article about functors that went over several examples of Functor types. It’s a collection of examples of how these can be useful in practice, but it’s a bit light on intuition. Let’s right that wrong today!

I’d recommend giving the above a quick read, as this post won’t spend much time going over what was said back then. tl;dr, Functor is just another typeclass (a structure just like Semigroup or Monoid) with one special method:

-- Any functor must have a `map` method:
map :: Functor f => f a ~> (a -> b) -> f b

… and (surprise!) a couple of laws. For any functor value u, the following must be true:

// Identity => x) === u

// Composition: === => g(f(x)))

They might not be immediately clear, and the definitions in the other article are a bit wordy, but have a couple reads. In my head, I tend to think to myself, a call to map must return an identical outer structure, with the inner value(s) transformed.

If this looks a bit weird to you, then it’s probably because Functor is the first entry we’ve seen in Fantasy Land that must contain some other type.

Think about String. It has actually ticked all the boxes up until now - it’s a Setoid, Semigroup, and a Monoid - but we can’t make it a Functor. This is because String is just, well, strings!

Array a has also ticked all the boxes so far, but it is a Functor. This is because it contains a value of type a (or, usually, several of them). Strings are just strings - you can’t have a “String of a thing” in the same way as you can have an “Array of a thing” or a “Maybe of a thing”.

Functor types are containers. Not all container types are functors. Want an example? Let’s look at Set a - a collection of unique values of some type a. There’s a catch here, though - a must be a Setoid. Looking at the type signature for map, we see no Setoid restrictions - we could map our Set a values to some not-a-Setoid type b (like Function), and we’d have no way of ensuring uniqueness!

A functor does not care about the type it’s holding. Set, on the other hand, must. Thus, Set can’t be a Functor. Don’t worry, though - there are plenty of types that are functors!

Ok, everything has hopefully just about made sense up to here. Now’s when we get a bit… hand-wavey. Sorry in advance. We’ve been over the laws, and we’ve seen some examples. What’s the intuition, though? If all these wildly different types of things are functors, what behaviour do they all share?

Imagine a world without functors. All we have are basic values: Int, String, Number, that sort of thing. No Array, though. No Set, either, as you’d internally need an array of values. No nullable values at all (they’re not type-safe). No Function, even! We’d have an extremely limited set of tools and would probably throw our computers out the window before too long. But, when we start introducing Functor types:

Type Capability
a Represent a value.
Maybe a Represent a possibly null value.
Either e a Represent a value or exception.
Array a Represent a number of values.
x -> a Represent a mapping to values.

A functor is like a little “world” in which our boring, functor-less language has been extended in some (hopefully useful!) way. When we put a value into one of these worlds, we say that we’re lifting the value into the functor.

More than one extension? What if we want to represent a number of mappings to values? We make an Array of Functions! What about a mapping to a possibly null value? We write a Function that maps to Maybes! If we want to combine these capabilities, we simply nest them.

We’ll see that we’re not always so lucky with composition-by-nesting when we get on to more complex structures!

We’re there. That’s what a functor does. It provides some extended behaviour, with total, gorgeous type safety. Note that our language isn’t changed inside a functor type - just extended. This is why the functor laws hold. Let’s write a couple of little proofs:

// For ANY functor *constructor* U:
// e.g. [x].map(f) === [f(x)]
U(x).map(f) === U(f(x))

// Read: `map` just applies the function to
// the inner value. Using only this rule,
// we win!

const id = x => x

// Identity...

  === U(id(x))

  === U(x) // id(x) === x

// Composition...

  === U(g(x)).map(f)

  === U(f(g(x)))

  === U(x).map(x => f(g(x)))

Our laws hold, everything works. If it helps your intuition, just think of U(x).map(f) === U(f(x)) every time you run into a functor. More generally (and correctly), map applies a function to the value(s) within a functor and nothing else. That, friends, is all there is to it!

So, I’m sorry that it got a bit less clearly-defined towards the end. You can see it’s quite difficult to define an all-encompassing intuition for functors, but I hope this has given you somewhere to start!

If you want more examples, the Haskell docs contain loads of Functor types for you to see! The intuition will come with time. Until then, as always, feel free to tweet me and I’ll do my best to clarify my ramblings.

Next time, we’ll look at another type of functor: the contravariant functor. Get excited! As always, until then,

Take care ♥