A recursive function is a computational scheme for constructing a value of a certain type in a stepwise, compositional way, i.e. via a range of recursive calls. For instance, the factorial of n is composed by gradual accumulation of products of the numbers from 1 to n. As much as this observation seems trivial, it ceases to be such once one starts to express such compositions with the convenient formalism known as Algebraic Data Types (ADTs). With ADTs, each value of a given type is not just an ‘anonymous’ element in a set, with no obvious relationship to the other elements (e.g. the value 2 in a ‘flat’ set of integers, meant just as an unstructured ‘bag’ of elements). Rather, a value is a combinatorial data structure that captures the compositional nature of that formal object: e.g. the fact that 2 is the third natural number and hence requires exactly two applications of a successor function to the number zero. Crucially, by considering values as combinatorial entities, the (by definition inductive) structure of ADTs naturally relates to the ways in which such structures can be constructed and processed, i.e. recursive functions. Also, the theory of ADTs reveals that, for most familiar types, just a handful of elementary compositions suffices to express all values and so facilitate a rich repertoire of ways in which they can be processed.
For these and other reasons, ADTs are ubiquitous in functional programming languages and type theory. The sections that follow introduce the basic concepts of the ADTs and link them to recursion schemes.
Algebraic Data Types
The most familiar example of an ADT is List, which may either be constructed as the empty list (Nil), or else as some element to be concatenated with an existing list. However, all familiar datatypes may be represented as ADTs (indeed, these are the only datatypes in the Haskell language, for example).
Formally, there are three fundamental constructions for defining new data types from existing ones S and T:
-
1.
Disjoint union the type containing either an instance of Sor an instance of T, denoted \(S+T\). For those used to the object-oriented (OO) perspective, this corresponds to inheritance (specialization) S and T can be considered as specializations of the type \(S+T\).
-
2.
Cartesian product denoted \(S \times T\), the type of pairs (s, t), where s is of type S, and t is of type t. This construction corresponds to composition (aggregation) in OO programming: an object of the type \(S \times T\) hosts objects of types S and T as its members.
-
3.
Exponentiation: the type of functions from S to T, denoted \(T^S\).
Listing 1 shows how ADTs can be represented even in a non-functional language such as Java: an IntList is either empty (Nil) or else constructed (Cons) from the concatenation of an integer value and some pre-existing list. IntList is therefore the disjoint union of Nil and Cons, or more formally, using the above notation:: \({ IntList}={ Nil}+{ Cons}\), while \({ Cons}={ int}\times { IntList}\). For simplicity of exposition, we discuss IntList rather than the more typical generic notion of List, which could be instantiated with an arbitrary element type, for example as a list of integers or a list of strings. Generic recursion schemes over arbitrary data types are discussed in Sect. 8.
In languages such as Haskell or Scala, IntList can be represented more succinctly. Listing 2 gives the analogous Scala code for IntList, together with a recursive version of the length function, implemented via pattern match against cases. Pattern matching can be seen as a mechanism that is complementary to the above presentation of ADTs as a means of constructing elements of a type, in allowing the deconstruction of a given object into its constituent components. Values can be matched against atomic values (like the case Nil() in Listing 2) or against object ‘templates’, like case Cons(head,tail)). Crucially, in the latter case, the constituents of the matched value (head,tail) become available as values that can undergo further processing.
The above approach is applicable to arbitrary ADTs, not just List (for which this sort of construct is more widely known). Moreover, it is possible for compiler to statically determine if a set of cases which pattern-match against an ADT is exhaustive. This capability is important, as it not only guarantees that all possibilities are being handled, but, as we will show later, enables us to ensure that recursion is well-founded.
The implementation of the Cons case in Listing 2 exemplifies the gradual accumulation of the result when execution traverses the recursive structure of the underlying ADT. Such accumulation is present in all recursive functions. The core of the approach presented in this paper is that this can be formulated in abstraction from a specific implementation of some recursive function. The arguably simplest and most well-known instance of this observation is the fold operation applied to lists. Folding a list of elements of some type (like Int in our ongoing example) into a value of some generic accumulator typeA (to be specified by the caller of the function) requires two components:
-
a value of type A to be returned when the input list is empty (which is typically a neutral element of type A), and
-
a function with signature (List, A)\(\implies \)A that accumulates the values of type A as the computation progresses along the list.
Listing 3 gives an alternative (w.r.t. to Listng 2) implementation of length using fold for IntList. As can be seen, it orchestrates the recursion via a case-specific template foldList [37], requiring the user of the function to provide three things: the list l to which the fold is to be applied; a value nilCase of type A to be returned when the Nil case is encountered, and a binary function \({ consCase: Cons}\,\times A \rightarrow A\) to be applied in all other cases.
Once foldList is defined, it can be instantiated with a specific components. For our length example, A=Int, nilCase=0 and consCase is given by the lengthConsCase function that simply increments the accumulator.
Catamorphisms
As shown in the above examples, by factoring out recursion, fold replaces explicit recursion with its implicit use. In previous GP work by Yu et al., List folds were used together with synthesised callbacks represented via lambda functions and applied to the even-parity problem [40], to Fibonnacci series, and to determine if a string is a substring of another [39].
However, fold on lists is actually a special case of a more general concept known as a catamorphism, which can be defined on all algebraic datatypes. The use of the prefix ‘cata’ (from the Greek \(\upkappa \upalpha \uptau \upalpha \)—“downwards”) refers to the fact that the recursion ‘descends’ through the structure of the object to which it is applied, peeling away a layer of structure at each recursive invocation and applying a specified transformation to the object constructor (in the pattern-matching clause) representing that layer.
For example, the above calculation for length on the 2-element list [1, 2], represented by nested constructors Cons(1, Cons(2, Nil())), successively descends through cases Cons(1, Cons(2, Nil())), followed by Cons(2, Nil()) and then Nil.
For brevity, catamorphisms are typically denoted via ‘banana-bracket’ notation [22], of the form
$$\begin{aligned} (\!\mid case_1, \ldots , case_n \mid \!), \end{aligned}$$
(1)
where each case corresponds to a element of the disjoint union expressing the ADT—some of which can be atomic values, while some are functions that determine how the outcome should be accumulated when the input value is being deconstructed by pattern matching. These elements correspond one-to-one to the consecutive clauses of the pattern matching implementation in our length example. The length of a List is succinctly expressed as
$$\begin{aligned} (\!\mid 0, (l,accumulator) \mapsto 1 + accumulator\mid \!), \end{aligned}$$
(2)
corresponding directly to the cases in Listing 3. Notice that this notation implicitly assumes that l is being matched against the corresponding ADT constructor (here, Cons(x,xs)).
The domain of lists has the didactic advantage of explicitly involving construction/deconstruction of well-known data structures that are widely considered as composite, and so illustrates the underpinnings of ADTs in a down-to-earth manner. However, virtually all familiar datatypes have such a compositional nature and can be thus be conveniently expressed with ADTs—it is just that this fact is commonly ignored, not least because in contemporary hardware architectures, the values of many types (like Int) are more familiar in terms of low-level implementations that obscure their underlying compositional nature.
Listing 4 gives a Scala ADT corresponding to the Peano definition of Nat, the type of natural numbers, \({\mathbb {N}}\), viz. that a natural number is either zero or the successor of some natural number. The listing also provides the catamorphism for Nat. Familiar functions on Nat are readily expressed as catamorphisms: for example, multiplication mul(n, m) is \((\!\mid 0, (pred,accumulator) \mapsto pred + m\mid \!)\).
Crucially, catamorphisms on Nat suffice to express every primitive recursive function [15].Footnote 1 The practical implication is that many commonly used recursive functions defined of Nat can be expressed by calling cataNat with appropriate arguments, in particular an appropriate accumulator function. For instance, the Fibonacci function is given by the following catamorphism:
$$\begin{aligned} (\!\mid (0, 1), (a, b) \mapsto (b, a + b) \mid \!). \end{aligned}$$
(3)
Despite the simplicity of its well-known definition, the factorial function is more readily expressible via the alternative recursion schemes of Sect. 8 than via a catamorphism.
By factoring out recursion, catamorphisms are thus immensely expressive and cover a wide range of primitive recursive functions. No wonder they are considered more and more often as a part of lingua franca of functional programming, moving up the level of discourse and facilitating more efficient development of more robust software.
In Sect. 8, we give examples of catamorphisms for ADTs other than List and Nat.
If the cases provided to a catamorphism collectively define a total function (i.e. there is one case for each element in the disjoint union, and each case is itself total), then termination is guaranteed. Non-termination is a frequent source of difficulty in the synthesis of recursive functions, and has been often addressed with ad hoc methods devised over the years. The source of the problem is ill-formed infinite recursive calls, or in the more general sense, viz. that even a minute modification of a recursive function may drastically affect the course of recursion calls and so impair candidate program quality. That brittleness has been pointed to in numerous past works on GP for recursive functions (see, e.g., [4, 23, 24, 40]). The well-foundedness of catamorphisms obviates this problem and addresses it in a principled manner.