Fantas, Eel, and Specification 6: Functor
27 Mar 2017Fantasy Landers, assemble! We’ve been concat
enating 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.
Another #functional blog post on #functors in #javascript, including Maybe, Either, and... Function :O https://t.co/3H5T0yb35w
— Tom Harding (@am_i_tom) 31 December 2016
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
u.map(x => x) === u
// Composition:
u.map(f).map(g) === u.map(x => 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 Function
s! What about a mapping to a possibly null value? We write a Function
that maps to Maybe
s! 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(x).map(id)
=== U(id(x))
=== U(x) // id(x) === x
// Composition...
U(x).map(g).map(f)
=== 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 ♥