Category Theory for programmers
📦 Chapter 4 · Containers that play nice with functions

A functor is just
a box with a map button

If you've ever called .map on a List or Option, you've used a functor. The shape changes — the idea doesn't. Let's see why List, Option and Either are all the same animal in different clothes.

In Chapter 1 we said categories are just objects and arrows that compose, obeying two laws — identity and composition. In this chapter we meet our first real structure built on top of those laws: a way to take a function on plain values and "lift" it so it operates on values trapped inside a container.

You've been doing it for years. Every time you wrote List(1,2,3).map(_ + 1) you used the only operation a functor offers. The cast: a container F[A] and a function A => B. The plot: get back an F[B] — same shape, transformed contents. The rules: don't cheat (we'll see what cheating looks like).

1Reach inside the box

A functor is a container with a "map a function over my contents" button. The button is called .map in Scala and fmap in Haskell. Same idea, two names.

Three quick examples:

  • List(1, 2, 3).map(_ + 1)  =  List(2, 3, 4) — the function hits every element.
  • Some(5).map(_ * 2)  =  Some(10) — if there's a value, transform it.
  • None.map(_ * 2)  =  None — empty box passes through untouched.

Notice what stays the same: the container's shape. A list of three stays a list of three. A None stays a None. Only the things inside change — and they only change if there are any to change.

The formal name for any container that ships with this kind of well-behaved .map is a functor.

Interactive · push a function through different containers Pick a container · pick a function · hit map
pick a container and a function

2The two laws (you've seen them before)

Back in Chapter 1 we boiled categories down to identity and composition. A functor earns its name by preserving both when it lifts functions. That's the whole reason the word exists.

Identity law — the do-nothing arrow stays a do-nothing arrow

If you map the do-nothing function over a container, you get back exactly what you started with. No surprises, no extra work, no rearranging.

xs.map(x => x)  ==  xs

Composition law — two maps fuse into one

Mapping f and then mapping g has to be the same as mapping the combined function g ∘ f in one pass. The container can't sneak in extra work between the two steps.

xs.map(f).map(g)  ==  xs.map(g compose f)

In Haskell-speak: fmap g . fmap f  ==  fmap (g . f). Same equation.

💡
These two laws are what make .map predictable. A compiler, a teammate, or your future self can rewrite xs.map(f).map(g) as xs.map(g compose f) and know nothing breaks. Any half-clever map-shaped function that breaks the laws is not a functor — even if its type signature fits.
Interactive · check the laws (and watch a fake functor break) Toggle the bad map to see the composition law fail
 
xs = [1, 2, 3]  ·  f = _ + 1  ·  g = _ · 2

3Three functors, same dance

Different shapes, identical move. Each container has its own answer to "how do I apply a function to my contents?" — but they all respect the two laws.

  • List[A] — zero or more A's. .map applies f to each element, position preserved.
  • Option[A] — zero or one A. .map applies f if there's a value; if it's None, the box stays empty.
  • Either[E, A] — exactly one value, either an A on the Right or an E on the Left. .map only touches the Right side. Lefts pass through.

"Right-biased" is the Scala/Haskell convention: errors live on the Left, the happy value on the Right, and .map walks the happy path. The Left is held as-is, like a sealed envelope.

🎁
Why this matters in real code: you can write one helper that takes "a value of type A" and works on a single value, a list of values, an optional value, or a result-or-error — without changing a single line. The container's .map handles "is there anything here? how many?" for you.

4Why the laws matter — a fake functor

To see the laws working, let's break one on purpose. Here's a "duplicating map" that looks like .map but secretly doubles the list:

def badMap[A,B](xs: List[A], f: A => B): List[B] = xs.map(f) ++ xs.map(f)

It has the right shape — takes a list and a function, returns a list of the right element type. But run it twice in a row vs. once with the composite and you get wildly different sizes:

  • badMap(badMap(xs, f), g) — first call doubles the list to 2n, second doubles again to 4n.
  • badMap(xs, g compose f) — single call, list stays 2n.

Same starting list, same functions, two different answers. The composition law is broken — so badMap isn't a functor, no matter how innocent it looks. (Try it in the law-checker above — flip the use badMap switch.)

⚠️
The takeaway: the laws aren't paperwork. They're what lets you trust .map to be a transparent passthrough that touches contents but never touches structure. Break them and your "container" becomes a tiny rule-bending interpreter — fun, sometimes, but no longer composable.

5The same idea, in code

Both languages expose the operation as a single method/function. The signature is the giveaway: a function from A to B becomes a function from F[A] to F[B] — and the F can be anything functor-shaped.

6Where the name "functor" comes from

Here's the punchline that gives functors their name. Start with a plain function. A functor doesn't just give you a way to push it through a container — it gives you a brand-new function on the container itself. Watch the arrow get wrapped:

Interactive · lift a function into a container Click a container to lift
f : Int → String

That's the trick. A functor is a recipe that does two things at once: it maps types (IntList[Int]) and it maps arrows (Int => StringList[Int] => List[String]). And it does the second one in a way that preserves identity and composition — the structure of the category itself. Hence: structure-preserving map between categories, a.k.a. a functor.

7Check yourself

3 questions · instant feedback 0 / 3

8A taste of what's next

You can now lift any function into any functor. That gets us a lot, but here's the next obvious question: what about moving values between containers? How do you turn a List[A] into an Option[A] — for every A, the same way? That's a natural transformation: an arrow not between values, not between types, but between functors themselves.

🌿
Up next · Chapter 5
Natural Transformations
If functors are containers, natural transformations are arrows BETWEEN containers — a recipe for turning every List of A into an Option of A, the same way regardless of what A is.