What's in a Fold: The Basic Catamorphism in recursion-schemes

March 10, 2017

This article is meant as an accessible introduction to the most basic recursion scheme, the catamorphism. It won’t engage in deep dives into theory, or survey practical motives for using recursion schemes – that will be covered by the further reading suggestions at the end. Rather, its main goal is simply offering a concrete presentation of how folds can be generalised. This presentation will be done in terms of the types and combinators used by the recursion-schemes library, so that the article doubles as an introduction to some of its key conventions.

foldr

The primeval example of a fold in Haskell is the right fold of a list.

One way of picturing what the first two arguments of foldr are for is seeing them as replacements for the list constructors: the b argument is an initial value corresponding to the empty list, while the binary function incorporates each element prepended through (:) into the result of the fold.

By applying this strategy to other data structures, we can get analogous folds for them.

It would make sense to capture this pattern into an abstraction. At first glance, however, it is not obvious how to do so. Though we know intuitively what the folds above have in common, their type signatures have lots of superficial differences between them. Our immediate goal, then, will be simplifying things by getting rid of these differences.

ListF

We will sketch the simplification using the tangible and familiar example of lists. Let’s return to the type of foldr .

With the cosmetic flip I had applied previously, it becomes:

The annoying irregularities among the types of the folds in the previous section had to do with the number of arguments other than the data structure (one per constructor) and the types of said arguments (dependent on the shape of each constructor). Though we cannot entirely suppress these differences, we have a few tricks that make it possible to disguise them rather well. The number of extra arguments, for instance, can be always be reduced to just one with sufficient currying:

The first argument is now a pair. We continue by making its two halves more like each other by converting them into unary functions: the first component acquires a dummy () argument, while the second one gets some more currying:

We now have a pair of unary functions with result type b . A pair of functions with the same result type, however, is equivalent to a single function from Either one of the argument types (if you are sceptical about that, you might want to work out the isomorphism – that is, the pair of conversion functions – that witnesses this fact):

At this point, the only extra argument of the fold is an unary function with result type b . We have condensed the peculiarities of the original arguments at a single place (the argument of said function), which makes the overall shape of the signature a lot simpler. Since it can be awkward to work with anonymous nestings of Either and pairs, we will replace Either () (a, b) with an equivalent type equipped with suggestive names:

That leaves us with:

The most important fact about ListF is that it mirrors the shape of the list type except for one crucial difference…

… namely, it is not recursive. An [a] value built with (:) has another [a] in itself, but a ListF a b built with Cons does not contain another ListF a b . To put it in another way, ListF is the outcome of taking away the recursive nesting in the list data type and filling the resulting hole with a placeholder type, the b in our signatures, that corresponds to the result of the fold. This strategy can be used to obtain a ListF analogue for any other data structure. You might, for instance, try it with the BTree a type shown in the first section.

cata

We have just learned that the list foldr can be expressed using this signature:

We might figure out a foldr implementation with this signature in a mechanical way, by throwing all of the tricks from the previous section at Data.List.foldr until we squeeze out something with the right type. It is far more illuminating, however, to start from scratch. If we go down that route, the first question that arises is how to apply a ListF a b -> b function to an [a] . It is clear that the list must somehow be converted to a ListF a b , so that the function can be applied to it.

We can get part of the way there by recalling how ListF mirrors the shape of the list type. That being so, going from [a] to ListF a [a] is just a question of matching the corresponding constructors.

project witnesses the simple fact that, given that ListF a b is the [a] except with a b placeholder in the tail position, where there would be a nested [a] , if we plug the placeholder with [a] we get something equivalent to the [a] list type we began with.

We now need to go from ListF a [a] to ListF a b ; in other words, we have to change the [a] inside ListF into a b . And sure enough, we do have a function from [a] to b …

… the fold itself! To conveniently reach inside ListF a b , we set up a Functor instance:

And there it is, the list fold. First, project exposes the list (or, more precisely, its first constructor) to our machinery; then, the tail of the list (if there is one – what happens if there isn’t?) is recursively folded through the Functor instance of ListF ; finally, the overall result is obtained by applying f to the resulting ListF a b .

One interesting thing about our definition of foldList is that all the list-specific details are tucked within the implementations of project , fmap for ListF and f (whatever it is). That being so, if we look only at the implementation and not at the signature, we find no outward signs of anything related to lists. No outward signs, that is, except for the name we gave the function. That’s easy enough to solve, though: it is just a question of inventing a new one:

cata is short for catamorphism, the fancy name given to ordinary folds in the relevant theory. There is a function called cata in recursion-schemes. Its implementation…

… is the same as ours, almost down to the last character. Its type signature, however, is much more general:

It involves, in no particular order:

b , the type of the result of the fold;

t , the type of the data structure being folded. In our example, t would be [a] ; or, as GHC would put it, t ~ [a] .

Base , a type family that generalises what we did with [a] and ListF by assigning base functors to data types. We can read Base t as “the base functor of t ”; in our example, we have Base [a] ~ ListF a .

Recursive , a type class whose minimal definition consists of project , with the type of project now being t -> Base t t .

The base functor is supposed to be a Functor , so that we can use fmap on it. That is enforced through a Functor (Base t) constraint in the definition of the Recursive class. Note, however, that there is no such restriction on t itself: it doesn’t need to be a polymorphic type, or even to involve a type constructor.

In summary, once we managed to concentrate the surface complexity in the signature of foldr at a single place, the ListF a b -> b function, an opportunity to generalise it revealed itself. Incidentally, that function, and more generally any Base t b -> b function that can be given to cata , is referred to as an algebra. In this context, the term “algebra” is meant in a precise technical sense; still, we can motivate it with a legitimate recourse to intuition. In basic school algebra, we use certain rules to get simpler expressions out of more complicated ones, such as ax + bx = (a + b)x. Similarly, a Base t b -> b algebra boils down to a set of rules that tell us what to do to get a b result out of the Base t b we are given at each step of the fold.

Fix

The cata function we ended up with in the previous section…

… is perfectly good for practical purposes: it allows us to fold anything that we can give a Base functor and a corresponding project . Not only that, the implementation of cata is very elegant. And yet, a second look at its signature suggests that there might be an even simpler way of expressing cata . The signature uses both t and Base t b . As we have seen for the ListF example, these two types are very similar (their shapes match except for recursiveness), and so using both in the same signature amounts to encoding the same information in two different ways – perhaps unnecessarily so.

In the implementation of cata , it is specifically project that links t and Base t b , as it translates the constructors from one type to the other.

Now, let’s look at what happens once we repeatedly expand the definition of cata :

This continues indefinitely. The fold terminates when, at some point, fmap c does nothing (in the case of ListF , that happens when we get to a Nil ). Note, however, that even at that point we can carry on expanding the definition, merrily introducing do-nothing operations for as long as we want.

At the right side of the expanded expression, we have a chain of increasingly deep fmap -ped applications of project :

If we could factor that out into a separate function, it would change a t data structure into something equivalent to it, but built with the Base t constructors:

We would then be able to regard this conversion as a preliminary, relatively uninteresting step that precedes the application of a slimmed down cata , that doesn’t use neither project nor the t type.

Defining omniProject seems simple once we notice the self-similarity in the chain of project :

Guess what happens next:

GHCi complains about an “infinite type”, and that is entirely appropriate. Every fmap -ped project changes the type of the result by introducing a new layer of Base t . That being so, the type of omniProject would be…

… which is clearly a problem, as we don’t have a type that encodes an infinite nesting of type constructors. There is a simple way of solving that, though: we make up the type we want!

If we read Fix f as “infinite nesting of f ”, the right-hand side of the newtype definition just above reads “an infinite nesting of f contains an f of infinite nestings of f ”, which is an entirely reasonable encoding of such a thing.

All we need to make our tentative definition of omniProject legal Haskell is wrapping the whole thing in a Fix . The recursive fmap -ping will ensure Fix is applied at all levels:

Another glance at the definition of cata shows that this is just cata using Fix as the algebra:

That being so, cata Fix will change anything with a Recursive instance into its Fix -wearing form:

Defining a Fix -style structure from scratch, without relying on a Recursive instance, is just a question of introducing Fix in the appropriate places. For extra convenience, you might want to define “smart constructors” like these two:

Before we jumped into this Fix rabbit hole, we were trying to find a leanCata function such that:

We can now easily define leanCata by mirroring what we have done for omniProject : first, we get rid of the Fix wrapper; then, we fill in the other half of the definition of cata that we left behind when we extracted omniProject – that is, the repeated application of f :

(It is possible to prove that this must be the definition of leanCata using the definitions of cata and omniProject and the cata f = leanCata f . omniProject specification. You might want to work it out yourself; alternatively, you can find the derivation in an appendix at the end of this article.)

What should be the type of leanCata ? unfix calls for a Fix f , and fmap demands this f to be a Functor . As the definition doesn’t use cata or project , there is no need to involve Base or Recursive . That being so, we get:

This is how you will usually see cata being defined in other texts about the subject.

Similarly to what we have seen for omniProject , the implementation of leanCata looks a lot like the cata we began with, except that it has unfix where project used to be. And sure enough, recursion-schemes defines…

… so that its cata also works as leanCata :

In the end, we did manage to get a tidier cata . Crucially, we now also have a clear picture of folding, the fundamental way of consuming a data structure recursively. On the one hand, any fold can be expressed in terms of an algebra for the base functor of the structure being folded by the means of a simple function, cata . On the other hand, the relationship between data structures and their base functors is made precise through Fix , which introduces recursion into functors in a way that captures the essence of recursiveness of data types.

To wrap things up, here a few more questions for you to ponder:

Does the data structure that we get by using Maybe as a base functor correspond to anything familiar? Use cata to write a fold that does something interesting with it.

What could possibly be the base functor of a non-recursive data structure?

Find two base functors that give rise to non-empty lists. One of them corresponds directly to the NEList definition given at the beginning of this article.

As we have discussed, omniProject / cata Fix can be used to losslessly convert a data structure to the corresponding Fix -encoded form. Write the other half of the isomorphism for lists; that is, the function that changes a Fix (ListF a) back into an [a] .

Closing remarks

When it comes to recursion schemes, there is a lot more to play with than just the fundamental catamorphism that we discussed here. In particular, recursion-schemes offers all sorts of specialised folds (and unfolds), often with richly decorated type signatures meant to express more directly some particular kind of recursive (or corecursive) algorithm. But that’s a story for another time. For now, I will just make a final observation about unfolds.

Intuitively, an unfold is the opposite of a fold – while a fold consumes a data structure to produce a result, an unfold generates a data structure from a seed. In recursion schemes parlance, the intuition is made precise by the notion of anamorphism, a counterpart (technically, a dual) to the catamorphism. Still, if we have a look at unfoldr in Data.List , the exact manner in which it is opposite to foldr is not immediately obvious from its signature.

One way of clarifying that is considering the first argument of unfoldr from the same perspective that we used to uncover ListF early in this article.

Further reading

Understanding F-Algebras, by Bartosz Milewski, covers similar ground to this article from an explicitly categorical perspective. A good follow-up read for sharpening your picture of the key concepts we have discussed here.

An Introduction to Recursion Schemes, by Patrick Thompson, is the first in a series of three articles that present some common recursion schemes at a gentle pace. You will note that examples involving syntax trees and simplifying expressions are a running theme across these articles. That is in line with what we said about the word “algebra” at the end of the section about cata .

Practical Recursion Schemes, by Jared Tobin, offers a faster-paced demonstration of basic recursion schemes. Unlike the other articles in this list, it explores the machinery of the recursion-schemes library that we have dealt with here.

*Functional Programming With Bananas, Lenses, Envelopes and Barbed Wire, by Erik Meijer, Maarten Fokkinga and Ross Paterson, is a classic paper about recursion schemes, the one which popularised concepts such as catamorphism and anamorphism. If you plan to go through it, you may find this key to its notation by Edward Z. Yang useful.

Appendix: leanCata

This is the derivation mentioned in the middle of the section about Fix . We begin from our specification for leanCata :

Take the left-hand side and substitute the definition of cata :

Substitute the right-hand side of the leanCata specification:

By the second functor law:

unfix . Fix = id , so we can slip it in like this:

Substituting the definition of omniProject :

Substituting this back into the specification: