A lot of people who like functional programming often give the reason that the functional style is simply more elegant than the imperative style. When compelled or inspired to explain (as I did in my old post, How I Learned to Love Functional Programming), they often point to the three “higher-order” functions map, fold, and filter, as providing a unifying framework for writing and reasoning about programs. But how unifying are they, really? In this post we’ll give characterizations of these functions in terms of universal properties, and we’ll see that in fact fold is the “most” universal of the three, and its natural generalization gives a characterization of transformations of standard compound data types.
By being universal or having a universal property, we don’t mean that map, fold, and filter are somehow mystically connected to all programming paradigms. That might be true, but it’s not the point of this article. Rather, by saying something has a universal property we are making a precise mathematical statement that it is either an initial or final object (or the unique morphism determined by such) in some category.
That means that, as a fair warning to the reader, this post assumes some basic knowledge of category theory, but no more than what we’ve covered on this blog in the past. Of particular importance for the first half of this post is the concept of a universal property, and in the followup post we’ll need some basic knowledge of functors.
Map, Filter, and Fold
Recalling their basic definitions, map is a function which accepts as input a list
In most languages, implementing the map function is an elementary exercise. Here is one possible definition in ML.
fun map(_, nil) = nil
| map(f, (head::tail)) = f(head) :: map(f, tail)
Next, filter takes as input a boolean-valued predicate
fun filter(_, nil) = nil
| filter(p, (head::tail)) = if p(head)
then (head :: filter(p, tail))
else filter(p, tail)
Finally, fold is a function which “reduces” a list
fun fold(_, v, nil) = v
| fold(f, v, (head::tail)) = f(head, fold(f, v, tail))
(If this definition is mysterious, you’re probably not ready for the rest of this post.)
One thing that’s nice about fold is that we can define map and filter in terms of it:
fun map(f, L) = fold((fn x, xs => (f(x):xs)), [], L)
fun filter(p, L) = fold((fn x, xs => if p(x) then x:xs else xs end), [], L)
We’ll see that this is no accident: fold happens to have the “most general” universality of the three functions, but as a warm-up and a reminder we first investigate the universality of map and filter.
Free Monoids and Map
Map is the easiest function of the three to analyze, but to understand it we need to understand something about monoids. A monoid is simple to describe, it’s just a set
A monoid homomoprhism between two monoids
We encounter simple monoids every day in programming which elucidate this definition. Strings with the operation of string concatenation form a monoid, and the empty string acts as an identity because concatenating a string to the empty string has no effect. Similarly, lists with list concatenation form a monoid, where the identity is the empty list. A nice example of a monoid homomorphism is the length function. We know it’s a homomorphism because the length of a concatenation of two lists is just the sum of the lengths of the two lists.
Integers also form a monoid, with addition as the operation and zero as the identity element. However, the list and string monoids have an extra special property that integers do not. For a number
Definition: Let
As usual with a definition by universal property, we should elaborate as to exactly what’s going on. Let
and whose morphisms are commutative diagrams of the form

free-object-cat-morphism
where
By saying that the free monoid on

free-object-univ-prop
For example, if
We will now restrict our attention to lists and types, and we will denote the free (list) monoid on a type

bad-map
And while this is true, the diagram lies because “map” does not achieve what

still-bad-map
then things work out nicely. In particular, specifying a function
Indeed, we’ve learned from this analysis that the structure of the list involved is crucial in forming the universal property. Map is far from the only computable function making the diagram commute, but it is clearly the only monoid homomorphism. As we’ll see, specifying the order of operations is more generally described by fold, and we’ll be able to restate the universal property of map without relying on monoid homomorphisms.
Filter
The filter function is a small step up in complexity from map, but its universal property is almost exactly the same. Again filter can be viewed through the lens of a universal property for list monoids, because filter itself takes a predicate and produces a list monoid. We know this because filtering two lists by the same predicate and concatenating them is the same as concatenating them first and then filtering them. Indeed, the only difference here is that the diagram now looks like this

filter-bad
where
Fold, and its Universal Property
Fold is different from map and filter in a very concrete way. Even though map and filter do specify that the order of the list should be preserved, it’s not an important part of their definition: filter should filter, and map should map, but no interaction occurs between different entries during the computation. In a sense, we got lucky that having everything be monoid homomorphisms resulted in preserving order without our specific intention.
Because it doesn’t necessarily produce lists and the operation can be quite weird, fold cannot exist without an explicit description of the order of operations. Let’s recall the definition of fold, and stick to the “right associative” order of operations sometimes specified by calling the function “foldr.” We will use foldr and fold interchangeably.
fun foldr(_, v, nil) = v
| foldr(f, v, (head::tail)) = f(head, foldr(f, v, tail))
Even more, we can’t talk about foldr in terms of monoids. Even after fixing
One first try would be to view foldr as a morphism of sets
The mapping is just
Then the left hand side of the mapping above is a set of size 8 (there are eight ways to combine a choice of element in
So what can we say about fold? The answer is a bit abstract, but it works out nicely.
Say we fix a type for our lists,

fold objects
By
Now the morphisms in

fold morphims
Here we write
Now it turns out that fold satisfies the universal property of being initial in this category. Well, not quite. We’re saying first that the following object

initial object fold
Where cons is the list constructor

fold-univ-property
This diagram has a lot going on in it, so let’s go ahead and recap. The left column represents an object
To prove all of this, we need to first show that the object
So if we have a morphism
Supposing that we have two such morphisms
where the last equality holds by the commutativity of the diagram. Continuing,
where the last equality holds by the inductive hypothesis. From here, we can reverse the equalities using
To show that fold actually makes the diagram commute is even simpler. In order to make the diagram commute we need to send the empty list to
So we’ve established that fold has this universal property. It’s easy now to see how map and filter fit into the picture. For mapping types
A skeptical reader might ask: what does all of this give us? It’s a good question, and one that shouldn’t be taken lightly because I don’t have an immediate answer. I do believe that with some extra work we could use universal properties to give a trivial proof of the third homomorphism theorem for lists, which says that any function expressible as both a foldr and a foldl can be expressed as a list homomorphism. The proof would involve formulating a universal property for foldl, which is very similar to the one for foldr, and attaching the diagrams in a clever way to give the universal property of a monoid homomorphism for lists. Caveat emptor: this author has not written such a proof, but it seems likely that it would work.
More generally, any time we can represent the requirements of a list computation by an object like
That’s right, I said it: there’s more to the world than lists. Shun me if you must, but I will continue dream of great things.
In an effort to make the egregiously long posts on this blog slightly shorter, we’ll postpone our generalization of the universal property of fold until next time. There we’ll define “initial algebras” and show how to characterize “fold-like” computations any compound data type.
Until then!
Want to respond? Send me an email, post a webmention, or find me elsewhere on the internet.