Functoriality
Bartosz Milewski ยท 2015 ยท Category Theory for Programmers, Ch. 8
Prereqs: ๐ Ch7 (Functors). 8 min.
Every algebraic data type you build from products, coproducts, and function types is automatically functorial. The functor instance writes itself because bimap distributes over sums and products, contramap handles the input side of functions, and dimap handles both sides at once.
Bifunctors: functors of two arguments
A bifunctor maps two categories into a third. In programming, that means a type constructor with two type parameters, where you can map over both independently. The canonical example: the pair type (a, b). Apply one function to the first component, another to the second.
Coproduct as a bifunctor
The coproduct (tagged union / Either) is also a bifunctor. bimap applies one function to the Left case, the other to the Right case. Together with the product bifunctor, these two give you the building blocks for all algebraic data types.
Maybe from bifunctors: algebraic data types are functorial
Maybe a = Either () a. The () is a constant (the Const functor), and a is the identity functor. Since Either is a bifunctor and both Const and Identity are functors, the composite is automatically a functor. Every ADT built from sums and products inherits its functor instance for free.
Contravariant functors: reversing the arrows
A contravariant functor is a functor from the opposite category. Instead of fmap : (a -> b) -> F a -> F b, you get contramap : (b -> a) -> F a -> F b. The arrows reverse. The canonical example: predicates. If you have a predicate on strings and a function int -> string, you can build a predicate on ints by converting first, then testing.
Comparators: another contravariant functor
Comparators are contravariant too. If you can compare strings, and you have a function person -> string that extracts a name, you can compare people by name. You pull back the comparison through the extraction function.
Profunctors: contra in first, co in second
The function arrow a -> b is contravariant in a (the input) and covariant in b (the output). A type that behaves this way is a profunctor. The operation dimap takes two functions: one to pre-process the input (going backwards), one to post-process the output (going forwards).
The Const functor
The Const functor ignores its second type parameter. Const c a always holds a c, regardless of a. Its fmap does nothing: the value doesn't depend on the type parameter, so there's nothing to transform. Const is the "zero" in the algebra of functors. It shows up everywhere in the automatic derivation of functor instances for ADTs.
Putting it together: the functoriality of ADTs
Every algebraic data type is a nested composition of sums (Either), products (pairs), Const, and Identity. Since all of these are functorial, their compositions are too. This is why Haskell can auto-derive Functor: the compiler walks the type structure and assembles fmap from bimap, contramap, and the identity. The only catch: the type parameter must appear in covariant (positive) position. If it appears in a function argument (negative position), you need Contravariant or Profunctor instead.
Notation reference
| Blog / Haskell | Scheme | Meaning |
|---|---|---|
| bimap f g | (bimap-pair f g p) | Map both components of a bifunctor |
| first f = bimap f id | (bimap-pair f (lambda (x) x) p) | Map only the first component |
| second g = bimap id g | (bimap-pair (lambda (x) x) g p) | Map only the second component |
| contramap f | (contramap-pred f pred) | Pre-compose: reverse the input arrow |
| dimap f g h | (dimap f g h) | Pre-compose f, post-compose g |
| Const c a | (make-const c) | Ignores type parameter a |
| Identity a | a | Passes type parameter through |
Neighbors
Milewski chapters
- ๐ Chapter 7 โ Functors: structure-preserving maps
- (next) Chapter 9 โ Function Types: exponentials
Paper pages that use these concepts
- ๐ Hedges 2018 โ open games use profunctors (contravariant coplay channel)
- ๐ Capucci 2021 โ parametrised optics generalize profunctors
Foundations (Wikipedia)
Translation notes
The blog post uses Haskell typeclasses (Bifunctor, Contravariant, Profunctor) and C++ templates. This page uses plain Scheme functions, since Scheme has no type system to enforce the laws. The automatic derivation of functor instances for ADTs, which Milewski shows via Const and Identity composition, is demonstrated here by manually following the same decomposition for Maybe and Tree. The blog post also discusses the Writer functor's derivation from Kleisli composition. That's covered in Chapter 4. One subtlety from the blog post worth noting: separate functoriality in each argument is not enough to prove joint functoriality. The commutativity condition (applying first then second equals applying second then first) is required. Categories that violate this are called premonoidal.