Abstract
In the tortoise-and-hare algorithm, when the fast pointer reaches the end of a finite list, the slow pointer points to the middle of this list. In the early 2000’s, this property was found to make it possible to program a palindrome detector for immutable lists that operates in one recursive traversal of the given list and performs the smallest possible number of comparisons, using the “There And Back Again” (TABA) recursion pattern. In this article, this palindrome detector is reconstructed in OCaml, formalized with the Coq Proof Assistant, and proved to be correct. More broadly, this article presents a compositional account of the tortoise-and-hare algorithm for finite lists. Concretely, compositionality means that programs that use a fast and a slow pointer can be expressed with an ordinary fold function for lists and reasoned about using ordinary structural induction on the given list. This article also contains a dozen new applications of the TABA recursion pattern and of its tail-recursive variant, “There and Forth Again”.
1 BACKGROUND AND INTRODUCTION
The tortoise-and-hare algorithm is nearly as old as Computer Science [23, Ex. 6, p. 7]. For example, it is used to detect in constant space whether a singly linked list is circular [37, Section 15.2]. The list is iteratively processed with two pointers, a slow one that traverses the list one element at a time and a fast one that traverses it two elements at a time. For finite lists as in, e.g., OCaml, the traversal terminates and when it does, either the fast pointer has reached the empty list or it has reached a singleton list. If it has reached the empty list, the given list has an even length—say \(2 \cdot n\) for some n—and the slow pointer points to its second half, which has length n. If it has reached a singleton list, the given list has an odd length—say \(2 \cdot n + 1\) for some n—and the slow pointer points to the suffix of the list that starts with its middle element and has length \(n + 1\).
A string is a palindrome when it is its own reverse: The same characters are enumerated when a palindrome is read from left to right and when it is read from right to left. Detecting whether a string is a palindrome is a time-honored programming problem, currently with solutions in 169 programming languages in the Rosetta Code web site [27]. For a string of length \(2 \cdot n\) or \(2 \cdot n + 1\), at most n comparisons suffice to decide whether it is a palindrome by comparing its two halves.
A singly linked finite list is a palindrome when it is its own reverse: The same elements are enumerated when a palindrome is traversed from left to right and when it is traversed from right to left. But unlike strings, which offer constant-time and constant-space access to their characters, accessing an element in a list takes linear time, and so accessing the elements of a list from right to left takes either constant space and quadratic time with the given list or linear space and linear time with a reversed copy of this list. For a list of length \(2 \cdot n\) or \(2 \cdot n + 1\), at most n comparisons suffice to decide whether this list is a palindrome, as for strings: one can compare half of this list with its reversed other half, as per the two equivalent characterizations of palindromes stated in Section 1.2. Detecting whether a list is a palindrome is a traditional exercise in transformational programming, with iterative solutions [5] [32, Ex. 3] [31, Section 2, p. 410]. A recursive solution exists that traverses the given list once, involves no list reversal, uses no other space than what is provided by the underlying control stack, and performs at most n comparisons for a list of length \(2 \cdot n\) or \(2 \cdot n + 1\) [11, Section 6]. This solution uses both the tortoise-and-hare algorithm and the There And Back Again (TABA) recursion pattern.
“TABA” was designed at the turn of the century [11]. It is a structural-recursion pattern where one data structure is traversed at call time (as usual) and another at return time (which is less usual). It is often cited to exemplify recursion and continuations [15, 26, 35, 36]. TABA has inspired a new calculation rule, IO swapping [28], it has proved useful to illustrate advances in type inference [17], and it has been used to express filters in XML [29], to process tail-aligned lists [19], to demonstrate introspection [1], and to achieve backpropagation compositionally [8, 43, 44]. TABA programs have been implemented in many programming languages, from Prolog and C to Haskell and Agda. At least two have been reasoned about in Why3 [16]. Only recently has it been formalized in a proof assistant [10]. The present work is a continuation of this formalization for slow and fast pointers.
In essence, to detect whether a list of length \(2 \cdot n\) or \(2 \cdot n + 1\) is a palindrome using TABA, one traverses this list in n recursive calls with a fast pointer and a slow pointer and one performs comparisons when returning, discontinuing the returns as soon as one of the comparisons fails. The present article reconstructs this solution and proves its correctness. More broadly, it shows how to reason about the tortoise-and-hare algorithm by induction over any given list, and how to implement this algorithm using a fold function for lists.
1.1 Roadmap
We first implement three instances of the tortoise-and-hare algorithm non-compositionally (Section 2) and then compositionally (Section 3). We then reason about the non-compositional implementations (Section 4) and then about the compositional implementations (Section 5). We also illustrate an iterative variant of TABA, “There and Forth Again” (Section 6), before concluding (Section 7).
This article starts with a “Dear Reader” figure (Figure 1, p. 2), and it ends with another (Figure 2, p. 33). To make it self-contained, we also provide background knowledge about the fold functions for lists (Appendix A), Johnsson’s lambda lifting and its left inverse (Appendix B), Ohori and Sasano’s lightweight fusion by fixed-point promotion and its left inverse (Appendix C), Reynolds’s defunctionalization and its left inverse (Appendix D), and the TABA recursion pattern (Appendix E).
Fig. 1. Dear reader, part 1/2.
Fig. 2. Dear reader, part 2/2.
1.2 Prerequisites and Notations
The reader is expected to have an elementary knowledge of recursive functions in the syntax of OCaml and of Gallina, the pure and total functional programming language used in the Coq Proof Assistant [3]. Gallina is pure and total in that, e.g., it features no computational effects (assignments, mutations, jumps) and all its programs terminate. In contrast, OCaml is impure in that it features jumps in the form of exceptions, and it is not total in that some of its functions are partial. For example, given a nonempty list, the library functions
Ditto for the function returning the last element of a list, which is defined recursively over the tail of the given list when this list is not empty:
In OCaml,
Semantically, some OCaml function abstractions and conditional expressions can commute when the formals do not occur in the test. So, for example, the two expressions
and
are observationally equivalent. (This commutation is used in Section 2.3.3 and in Appendix D.1 with a match-expression instead of an if-expression.) Likewise, applications may commute with conditional expressions. So, for example, if
and
are observationally equivalent. (This commutation is used in Section 3.3.)
The reader is also expected to have a knowledge of the Coq Proof Assistant that is commensurate with understanding a theorem such as the following one:
In words: a polymorphic list is the reverse of itself (i.e., a palindrome) if and only if
— | either it is the concatenation of a prefix list and a suffix list that are each other’s reverse, | ||||
— | or it is the concatenation of a prefix list and a suffix list with one element in the middle and this prefix list and this suffix list are each other’s reverse. | ||||
(This theorem has some significance for the two last programming exercises in Figure 1.)
For the rest, the development is calculational and hinges on fold functions for lists, on lexical scope and block structure, on relocating contexts, on representing functions as first-order data types and vice versa, and on the TABA recursion pattern—which are all reviewed in the appendices.
And in Figure 1, the exercise about convolving a list with itself illustrates the TABA recursion pattern; Parts (1)–(3) of the exercise about halving a list illustrate the Tortoise-and-Hare algorithm, and Part (4) illustrates both the Tortoise-and-Hare algorithm and the TABA recursion pattern; and the palindrome exercise illustrates both the Tortoise-and-Hare algorithm and the TABA recursion pattern. This algorithm and this recursion pattern are independent—not all data structures benefit from being traversed with a slow and fast pointer, and not all recursion patterns benefit from traversing another data structure at return time—but they can synergize, as demonstrated in the present article.
2 IMPLEMENTING THE TORTOISE-AND-HARE ALGORITHM
This section presents three successive instances of the tortoise-and-hare algorithm: one instance to compute the second half of a list, disregarding its middle element if it has one (Section 2.1), one instance to compute the second half of a list together with a reversed copy of its first half, also disregarding its middle element if it has one (Section 2.2), and one instance to detect whether a list is a palindrome (Section 2.3), comparing the second half of the given list with the reversed copy of its first half in varying fashions.
2.1 Computing the Second Half of a List
The goal of this section is to implement an OCaml function that, given a list, returns its second half, disregarding its middle element if it has one, as captured in the following unit test:
This unit test is composed of a series of increasing lists, to provide an inductive sense of how each of its successive results is obtained.
More formally, the OCaml function is specified as follows:
Given two lists
vs andv2s , applying this function tovs yieldsv2s if and only if there exists a listv1s that has the same length asv2s and is such thatvs is the result ofList.append v1s v2s or ofList.append v1s (m :: v2s) , for some list elementm .
In that light, the unit-test function above is more tellingly written as follows (
In order to traverse the list only once, and as per the tortoise-and-hare algorithm, let us use a fast pointer (
In words,
— | When processing a list (third clause), | ||||
— | The given list has an even length if | ||||
— | The given list has an odd length if | ||||
Let us analyze
— | This implementation is iterative: given a list of length 2n or \(2n+1\), it performs \(n+1\) tail calls to | ||||
— | This implementation is structurally recursive in that In a compositional implementation, there should be only one base case and in the induction step, the recursive call should be on the tail of the list, not on the tail of its tail. Section 3.1 presents such a compositional implementation. | ||||
2.2 Computing the Second Half and the Reversed First Half of a List
The goal of this section is to implement an OCaml function that, given a list, ignores its middle element if it has one, and returns its second half together with its reversed first half, as captured in the following unit test:
More formally, the OCaml function is specified as follows:
Given three lists
vs ,v1s , andv2s , applying this function tovs yields(v1s, v2s) if and only ifv1s andv2s have the same length and are such thatvs is the result ofList.append (List.rev v1s) v2s or ofList.append (List.rev v1s) (m :: v2s) , for some list elementm .
In a nutshell, we revisit Section 2.1, also parameterizing
In words,
— | In the third clause, every progress for the slow pointer so far (i.e., | ||||
— | In the first clause, the reversed prefix and the second half of the given list are returned. | ||||
— | In the second clause, the reversed prefix and the second half of the given list are also returned. And as in | ||||
Like
2.3 Detecting Whether a List is a Palindrome
The goal of this section is to implement an OCaml function that, given a list, detects whether this list is a palindrome. Section 2.3.1 compares the second half of the given list with the reversed copy of its first half, naively. Section 2.3.2 compares the second half of the given list with the reversed copy of its first half, more perspicuously since the second half and the reversed copy of the first half have the same length. Section 2.3.3 suggests representing the reversed copy of the first half as a function that performs the comparison with the second half and presents a refunctionalized version of the palindrome detector of Section 2.3.2 based on this functional representation. This refunctionalized version is in continuation-passing style and Section 2.3.4 presents its direct-style counterpart. Finally, Section 2.3.5 depicts the computation performed by these two palindrome detectors.
2.3.1 Detecting Whether a List is a Palindrome, Naively.
Based on Section 2.2, a palindrome detector can be naively implemented by first computing the suffix and the reversed prefix and then comparing them, which is correct by the theorem about the two equivalent characterizations of palindromes in Section 1.2:
2.3.2 Detecting Whether a List is a Palindrome, More Perspicuously.
The naive implementation of Section 2.3.1 works, but it is overly general since OCaml’s resident comparison function does not exploit the fact that its two arguments have the same length. Here is a more perspicuous comparison function that does exploit this fact:
Besides replacing
The body of
2.3.3 An Alternative Representation of the Reversed Prefix—Refunctionalization.
In the implementations of Section 2.3, the reversed prefix is solely constructed in order to be compared with the second half of the given list, as per the two following questions and answers:
Question: The initial reversed prefix, i.e., the empty list, is constructed in
pal2_fused . How is this empty list consumed in the perspicuous comparison function?Answer: Given this empty list and an empty suffix, this comparison function returns
true .Question: The reversed prefix is extended in
pal2_fused . How is this extension consumed in the perspicuous comparison function?Answer: Given this extended reversed prefix and a non-empty suffix, this comparison function compares their heads and returns
false if they are not equal. Otherwise, it continues to compare the reversed prefix and the tail of the non-empty suffix.
In the third clause of
Replacing the reversed prefix and
That the reversed prefix and
And indeed:
— | The empty list in the initial call to | ||||
— | Given (1) a slow pointer In this new continuation, short-circuit evaluation is still in force, i.e., the continuation is applied only if the two list elements are the same. Otherwise, the continuation is not applied, which has the effect of stopping the comparison and discontinuing the computation: the list is not a palindrome and the result of the predicate is | ||||
Here is the refunctionalized version of
Let us analyze
— | The auxiliary function | ||||
— | If the initial continuation is applied, the given list is a palindrome and the result is | ||||
— | As in Sections 2.1 and 2.2, | ||||
— | The intermediate list is traded for a continuation. | ||||
— | As it happens, this implementation has the hallmark of TABA (see Appendix E) in that it traverses the given list and grows a continuation to apply to its second half in order to traverse it. | ||||
— | Any given list of length \(2 \cdot n\) or \(2 \cdot n + 1\) is traversed with \(n + 1\) tail calls to | ||||
The accompanying
2.3.4 Back to Direct Style.
The definition of
The following analysis of
— | The auxiliary function | ||||
— | If the initial call to | ||||
— | Like | ||||
— | The continuation is traded for recursive calls. | ||||
— | This implementation also has the hallmark of TABA (see Appendix E) in that it traverses the given list at call time and its second half at return time. | ||||
— | Any given list of length \(2 \cdot n\) or \(2 \cdot n + 1\) is traversed with \(n + 1\) calls to | ||||
The accompanying
2.3.5 A Depiction.
The resulting two implementations,
Initial call:
The horizontal bar represents the given list, and its middle is marked with
m . In the initial call tovisit , bothfp andsp point to the beginning of this given list.Intermediate call:
As the calls to
visit progress,fp points twice as far in the given list thansp .Last call:
In the last call to
visit ,fp points to the end of the given list andsp points to its middle.First return (i.e., first application of the continuation):
In the first return from
visit , bothsp andsfx point to the middle of the given list.Intermediate return (i.e., application of an intermediate continuation):
As the returns from
visit progress,sp points to longer and longer suffixes of the given list and, symmetrically,sfx points to shorter and shorter suffixes of the given list. The returns continue for as long as the list elements pointed at bysp andsfx are the same ones, and otherwise the computation stops.Final return (i.e., application of the initial continuation):
The final return takes place if all the list elements successively pointed at by
sp andsfx were equal, i.e., if the given list is a palindrome.
2.4 Summary and Conclusion
This section presented three instances of the tortoise-and-hare algorithm: one to compute the second half of a list, one to compute the second half of a list together with a reversed copy of its first half, and one to detect whether a list is a palindrome. The last one illustrates refunctionalization, the TABA recursion pattern, and mapping an implementation back to direct style.
In all these implementations, consider the “fast suffix” of the given list denoted by
The accompanying
3 IMPLEMENTING THE TORTOISE-AND-HARE ALGORITHM, COMPOSITIONALLY
The goal of this section is to revisit each step of Section 2 in a compositional way. As a result, each successive implementation can be expressed as an instance of
3.1 Computing the Second Half of a List, Compositionally
The goal of this section is the same as the goal of Section 2.1: to implement an OCaml function that, given a list, yields its second half, ignoring its middle element if it has one. The implementation there used the tortoise-and-hare algorithm:
Here, we want
In words,
— | The given list has an even length if | ||||
— | When processing a list (second clause), | ||||
Let us analyze
— | This implementation is iterative: given a list of length n, it performs \(n + 1\) tail calls to | ||||
— | This implementation is compositional in that its pattern of recursion follows the pattern of induction in the construction of lists. In other words, Being tail recursive, this implementation can also be expressed as an instance of The alert reader will have noticed that in | ||||
3.2 Computing the Second Half and the Reversed First Half of a List, Compositionally
The goal of this section is the same as the goal of Section 2.2: to implement an OCaml function that, given a list, returns its second half together with its reversed first half, ignoring its middle element if it has one. The implementation there used the tortoise-and-hare algorithm:
Here, we proceed as in Section 3.1, also parameterizing
In words,
— | the initial value of the fast pointer is paired with the empty reversed prefix; | ||||
— | in the base case, the reversed prefix is also returned (if the given list has an odd length (i.e., if | ||||
— | in the induction step, every progress for the slow pointer so far (i.e., | ||||
Like
3.3 Detecting Whether a List is a Palindrome, Compositionally
The goal of this section is the same as the goal of Section 2.3: to implement an OCaml function that, given a list, detects whether this list is a palindrome.
Based on Section 2.2, a palindrome detector can be naively implemented by first computing the suffix and the reversed prefix and then comparing them:
As in Section 2.3.2, let us also inline the call to
As in Section 2.3.2, the body of
For cosmetic value, let us curry
Let us analyze
— | The auxiliary function | ||||
— | If the initial continuation is applied, the given list is a palindrome and the result is | ||||
— | This implementation is compositional and so it can be expressed as an instance of a fold function for lists. | ||||
— | The intermediate list is traded for a continuation. | ||||
— | As it happens, this implementation has the hallmark of TABA in that it traverses the given list and grows a continuation to apply to its second half in order to traverse it. | ||||
— | Having commuted the tail call to | ||||
The accompanying
As in Section 2.3.4, this implementation can be mapped back to direct style, using an exception to account for the discontinuity:
Our analysis of
The accompanying
3.4 Summary and Conclusion
This section distilled the essence of programming the tortoise-and-hare algorithm compositionally with three successive instances of this algorithm, the last one of which illustrates the TABA recursion pattern. The next two sections distill the essence of reasoning about this algorithm, first in a non-compositional way (Section 4) and then in a compositional way (Section 5).
As in Section 2, all the initial calls to
The accompanying
4 REASONING ABOUT THE TORTOISE-AND-HARE ALGORITHM
Let us switch from OCaml to Gallina, the pure and total functional programming language used in the Coq Proof Assistant. This section revisits the successive non-compositional implementations from Section 2 that compute the second half of a list (Section 4.1), that compute the second half of a list together with a reversed copy of its first half (Section 4.2), and that detect whether a list is a palindrome (Section 4.3), using Gallina. We then establish their correctness.
But first, let us capture the logic of slow and fast pointers when the fast pointer proceeds two steps at a time. Letting
This definition formalizes the informal characterization “the fast pointer is twice as fast as the slow pointer, and therefore is twice as far in the given list” in Section 2.1. It is to be used in Section 4.1.
Sections 4.2 and 4.3 will need a version of this alignment that is also parameterized by
4.1 Computing the Second Half of a List
The goal of this section is to prove the correctness of the non-compositional OCaml implementation from Section 2.1. Given a list, this function returns its second half.
The OCaml implementation uses
The accompanying
Inlining the definitions of
For simplicity, the auxiliary function is not local, as in Section 2.1, but is declared globally. (In technical terms,
The following lemma captures the intent of
where
— | The alignment premiss of the lemma is that since the fast pointer, | ||||
— | With this premiss, applying | ||||
— | This lemma also captures that | ||||
That
Incidentally, since the pattern of recursion does not follow the pattern of induction in the construction of lists, to simplify matters, we formalized a matching ad-hoc induction principle in the accompanying
4.2 Computing the Second Half and the Reversed First Half of a List
The goal of this section is to prove the correctness of the non-compositional OCaml function from Section 2.2, following the structure of Section 4.1: specification, proof that at most one function satisfies this specification, soundness of the unit tests from Section 2.2, implementation in Gallina as a main function and an auxiliary function, lemma capturing the intent of the auxiliary function, proof of this lemma using
4.3 Detecting Whether a List is a Palindrome
The goal of this section is to prove the correctness of all the non-compositional OCaml functions from Section 2.3. Given a list, these functions detect whether this list is a palindrome. For brevity, we only treat the one in direct style (Section 2.3.4), deferring to the accompanying
The palindrome detector is polymorphic in that it is parameterized with a type. Short of eqtypes as in Standard ML, it is also parameterized with an equality predicate:
Since Gallina is total, the result of refunctionalization and going back to direct style needs to be expressed with one option type in
In the definition of
The following lemma captures the intent of
In words:
— | The premises of the lemma are (1) that the comparison predicate for the list elements is sound and complete, and (2) that the fast pointer is twice as far as the slow pointer in the given list. | ||||
— | With these premises, applying | ||||
— | This lemma also captures that | ||||
This lemma is proved by induction on the given list, using
4.4 Summary and Conclusion
This section distilled the essence of reasoning about the tortoise-and-hare algorithm for detecting palindromes in lists by following the structure of these lists, exploiting the logic of slow and fast pointers and using an ad-hoc induction principle over the given list to account for the slow pointer.
5 REASONING ABOUT THE TORTOISE-AND-HARE ALGORITHM, COMPOSITIONALLY
This section revisits the successive compositional implementations in Section 3 that compute the second half of a list (Section 5.1), that compute the second half of a list together with a reversed copy of its first half (Section 5.2), and that detect whether a list is a palindrome (Section 5.3), using Gallina. We then establish their correctness.
But first, let us capture the logic of slow and fast pointers when the fast pointer proceeds one step at a time. The notion of alignment is still quantified with
This definition formalizes the informal characterization “the fast pointer is twice as fast as the slow pointer, and therefore is either twice as far or twice as far, plus 1, in the given list” in Section 3.1. It is to be used in Section 5.1.
Sections 5.2 and 5.3 will need a version of this alignment that is also parameterized by
5.1 Computing the Second Half of a List, Compositionally
The goal of this section is to prove the correctness of the compositional OCaml implementation from Section 3.1. Given a list, this function returns its second half.
As in Section 4.1, the partial functions
The following lemma captures the intent of
In words:
— | The alignment premiss of the lemma is that since the fast pointer is twice as fast as the slow pointer, it is either twice as far or twice as far, plus 1, in the given list. | ||||
— | With this premiss, applying | ||||
— | This lemma also captures that | ||||
That
Since the pattern of recursion follows the pattern of induction in the construction of lists, we proved this lemma by ordinary induction on the given list.
5.2 Computing the Second Half and the Reversed First Half of a List, Compositionally
The goal of this section is to prove the correctness of the compositional OCaml function from Section 3.2, following the structure of Section 5.1: implementation in Gallina as a main function and an auxiliary function, lemma capturing the intent of the auxiliary function, proof of this lemma using ordinary induction, and proof that the main function satisfies the specification as a corollary of the lemma. For brevity, we defer to the accompanying
5.3 Detecting Whether a List Is a Palindrome, Compositionally
The goal of this section is to prove the correctness of all the compositional OCaml functions from Section 3.3. Given a list, these functions detect whether this list is a palindrome. For brevity, we only treat the ones in continuation-passing style the one in direct style, deferring to the accompanying
5.3.1 Continuation-Passing Style.
As in Section 4, the result of refunctionalization is also expressed with an option type, due to the ripple effect of encoding
The following lemma captures the intent of
In words:
— | The premises of the lemma are (1) that the comparison predicate for the list elements is sound and complete, and (2) that the fast pointer is either twice as far as the slow pointer or twice as far, plus 1, in the given list. | ||||
— | With these premises, for any continuation | ||||
— | This lemma also captures that | ||||
This lemma is proved by induction on the given list. Thanks to the decomposition of this list into
The correctness of
5.3.2 Direct Style.
The codomain of
The following lemma captures the intent of
The situation is much the same as for Lemma
— | The premises of the lemma are the same: The comparison predicate for the list elements should be sound and complete and the fast pointer should be twice as far as the slow pointer or twice as far, plus 1, in the given list. | ||||
— | With these premises, applying | ||||
— | This lemma also captures that | ||||
This lemma is proved by induction on the given list, using equational reasoning.
Again, the correctness of
5.4 Summary and Conclusion
This section distilled the essence of reasoning compositionally about the tortoise-and-hare algorithm for detecting palindromes in lists by following the structure of these lists, exploiting the logic of slow and fast pointers and using ordinary induction over the given list.
6 THERE AND FORTH AGAIN
This section illustrates the synergy between the tortoise-and-hare algorithm and a tail-recursive variant of TABA, There and Forth Again (TAFA). To this end, instead of detecting whether a given list is the concatenation of a list and its reverse, possibly with an element in the middle (i.e., a palindrome), let us detect whether a given list is the concatenation of a list and its copy, possibly with an element in the middle (i.e., a “lapindrome,” to coin a term). This implementation should pass the following unit test:
More formally, the desired OCaml function is specified as follows:
Applying this function to a given list
vs yieldstrue if and only if there exists a listws which is such thatvs is the result ofList.append ws ws or ofList.append ws (m :: ws) , for some list elementm .
The implementation is a simple variant of the palindrome detector —at return time, the second half of the given list is compared with its first half (instead of with the reverse of its first half):
Unlike in
In words:
— | |||||
— | |||||
TAFA was introduced in the recent formalization of TABA [10]. It was illustrated with (1) indexing a list from right to left and (2) finding the longest common suffix of two given lists, an example due to Hemann and Friedman [19]. One of the exercises in Figure 2, p. 33, can be solved using TAFA.
7 CONCLUSION AND PERSPECTIVES
The tortoise-and-hare algorithm for finite lists can be implemented compositionally and reasoned about inductively. Combined with the TABA recursion pattern, it makes it possible, e.g., to detect whether a given list is the concatenation of a list and a reverse of this list, possibly with an element in the middle (i.e., a palindrome) in one recursive traversal of this given list and with the smallest possible number of comparisons. Combined with the TAFA tail-recursion pattern (see Section 6), it makes it possible, e.g., to detect whether a list is the concatenation of a list and a copy of this list, possibly with an element in the middle, in a similar fashion and with the smallest possible number of comparisons. Traversing a list one element at a time with the fast pointer and one element every second time with the slow pointer vs. two elements at a time with the fast pointer and one element at a time with the slow pointer illustrates Blum’s speedup theorem for lists [7]. This theorem can be illustrated further with more pointers, e.g., three (a slow pointer stepping through the list one element at a time, a medium pointer stepping through the list two elements at a time, and a fast pointer stepping through the list three elements at a time) or four, as in one of the exercises in Figure 2, p. 33. The tortoise-and-hare algorithm also provides a new class of examples where Bird and Wadler’s second duality theorem applies: these examples can be equivalently expressed by folding left and by folding right over the given lists.
To reason about TABA programs, we used an option type to account for the fact that the intermediate results were not empty lists. Using an option type in Gallina means that the program can be transliterated into any other programming language (e.g., OCaml). Alternatively, one could use dependent types and pair a nonempty list with a proof that this list is nonempty [3]. One could also wish for a more expressive static type system that accounts for non-empty lists [17] to avoid type warnings, run-time assertions, and the option type for list values that are known to not be the empty list [21, 22], as per Gordon Plotkin, and Robin Milner’s vision that for statically typed programs, reduction never gets stuck and evaluation never goes wrong. This more expressive type system could be used for trees. Suppose we are given an ordered tree of payloads and a payload. If this given payload occurs at depth n in the given tree, we want to return the subtree of depth \(n/2\) that contains this payload, or the context of this subtree (possibly reversed). It seems natural to traverse the tree using a fast pointer to search for the payload and a slow pointer that is half as far from the root as the fast pointer. After the first recursive call, the slow pointer always points to a node, and never to a leaf. Could this property be accounted for statically by a type system?
Since TABA was published, not very many new examples have emerged that illustrate this recursion pattern. The present article features a dozen such new examples, and so does the recent formalization of TABA [10]. These examples also provide new illustrations for Reynolds’s defunctionalization, for Ohori and Sasano’s lightweight fusion by fixed-point promotion, and for the CPS transformation as well as for their left inverses.
In addition, these new TABA examples and their formalization illustrate—at least to the author —how proof assistants can compound the Platonistic joy of programming recursive functions.
As the saying goes,
to understand recursion, one first needs to understand recursion.
But once one understands induction, one can understand induction.
APPENDICES
A FOLDING LEFT AND RIGHT OVER FINITE LISTS
The names “fold right” and “fold left” are due to David Turner [40], but these functionals are also known under other names, e.g., “reduce” and “accumulate” in Lisp [37]. They are due to Christopher Strachey in the early 1960’s [38] as an abstraction of structural recursion over lists (
And indeed applying
whereas applying
a concrete manifestation of the reverse order induced by accumulation.
In his Introduction to the Theory of Lists [6], Richard Bird swapped the order of arguments of the second argument of
whereas applying
a concrete manifestation of the eponymous laterality of association induced by each fold function.
As detailed elsewhere (A brief history of folding left and right over lists [9, App. A]), this swapped order was used in Bird and Wadler’s textbook [4] and it was adopted in Release 2 of Miranda so that Miranda could be used with this textbook. That opened the door to the varieties of fold functions for lists in functional languages today. For example, in OCaml, the library functions
This difference of types makes it complicated to refactor a program from using either fold function to using the other. In contrast, using Strachey’s original design, where
Also, the single premiss—left permutativity—is a lot simpler than the two eye-crossing premises in the second duality theorem. And last but not least, refactoring a program from using either fold function to using the other is achieved by merely changing the name of this fold function—e.g., to go from
So for example, since
Less so for the following two definitions of the length function for lists, which are also equivalent:
To close, the type of the result of a fold function is not restricted be a first-order type—it can be a function type too. For example,
A similar higher-order scheme makes it possible to express
That said, left permutativity is infrequent: in the definition just above,
B LAMBDA LIFTING AND LAMBDA DROPPING
In Section 2.1, the global definitions of
For another example, the following function
Recursive equations were deemed a convenient format for implementing functional programs, and so a program transformation—lambda lifting—was developed to map block-structured programs into global recursive equations [20].
On the other hand, the more recursive equations, the more expensive to construct their control-flow graph. Also, the more function parameters, the more expensive to construct the data-flow graph and the more pressure on the register allocator in the compiler. For these reasons, a converse program transformation—lambda dropping—was developed to map recursive equations into block-structured programs [13]. The lambda-dropped version of
In this lambda-dropped version, the auxiliary version (traditionally named
In Section 3, all OCaml functions are lambda dropped, for conciseness, and in Section 5, all Gallina functions are lambda lifted, for convenience. Otherwise, we would need to declare local lemmas for reasoning about local recursive functions, which is doable, but not as convenient as declaring global lemmas.
C LIGHTWEIGHT FUSION BY FIXED-POINT PROMOTION AND LIGHTWEIGHT FISSION BY FIXED-POINT DEMOTION
In the mid 2000’s [30], Ohori and Sasano developed a program transformation for relocating the context of the initial call to a tail-recursive function to the return point(s) in the body of this function. Its left inverse is logically named “lightweight fission by fixed-point demotion,” though it had been found to be of practical use earlier on [18]. Here is a simple example [9, Section 1.7.5]:
In both definitions,
D DEFUNCTIONALIZATION AND REFUNCTIONALIZATION
Defunctionalization [33, 34] is a generalization of closure conversion [24] to represent higher-order functions as first-order data, using control-flow analysis [25]. A program is first order when all its function calls are to named functions, and it is higher order when some of its functions (or their name) are used, e.g., as a parameter or as a result. A typical higher-order program involves a map function, a fold function, or continuations. A closure represents a function by pairing the body of this function together with its lexical environment, possibly restricted to the variables that occur free in this body. Defunctionalization groups the closures that are applied at the same place into a sum type. Each summand contains the values of free variables of the corresponding function. Evaluating a function abstraction is thus achieved by constructing a tagged collection of the values of its free variables, and applying this tagged collection is achieved by dispatching on the tag, creating a lexical environment, extending this lexical environment with the actual parameters, and evaluating the body of the function abstraction in this extended lexical environment.
D.1 An Example of Defunctionalization
Consider the continuation-passing implementation from Section 3.3:
The continuation has type
— | one in the initial call to | ||||
— | one in the body of | ||||
The resulting functional values are applied in the nil case and in the cons case of
Since continuations are instances of two function abstractions, their type can be represented using a data type with two constructors:
The first function abstraction has no free variables and the second one has two,
The corresponding defunctionalized program reads as follows:
The vigilant reader will have noticed that since
And since the type
D.2 Refunctionalization
Analyzing the image of defunctionalization shows that it gives rise to data structures that are constructed once and dispatched upon once. On this ground, a converse program transformation?refunctionalization?was developed to map programs where data structures are constructed once and dispatched upon once into higher-order programs where these data structures and their dispatch are represented as functions and their applications [12]. Section 2.3.3 illustrates how
E THERE AND BACK AGAIN
“There and Back Again” is a recursion pattern in which one data structure is traversed at call time and another at return time [11]. Its canonical example – the symbolic convolution of two lists in one pass – and its solution with continuations are due to Mayer Goldberg. For self-convolving a list, this solution reads as follows:
This implementation has the hallmark of TABA in that
This solution can be expressed in direct style as follows:
This implementation also has the hallmark of TABA in that
As in Sections 2 and 3, the uses of
- [1] . 2018. Collapsing towers of interpreters. Proceedings of the ACM on Programming Languages 2, POPL (2018), 52:1–52:33.
DOI: Google ScholarDigital Library
- [2] . 2004. Linear and Affine Typing of Continuation-Passing Style. Ph. D. Dissertation. Queen Mary, University of London.Google Scholar
- [3] . 2004. Interactive Theorem Proving and Program Development. Springer.Google Scholar
Digital Library
- [4] . 1988. Introduction to Functional Programming. Prentice-Hall International.Google Scholar
Digital Library
- [5] . 1984. Using circular programs to eliminate multiple traversals of data. Acta Informatica 21 (1984), 239–250. Google Scholar
Digital Library
- [6] . 1986. An Introduction to the Theory of Lists.
Technical Monograph PRG-56. Oxford University, Computing Laboratory, Oxford, England.Google Scholar - [7] . 1967. A machine-independent theory of the complexity of recursive functions. Journal of the ACM 14, 2 (1967), 322–336.Google Scholar
Digital Library
- [8] . 2019. Backpropagation in the simply typed lambda-calculus with linear negation. Proceedings of the ACM on Programming Languages 4, POPL (2019), 64:1–64:27.
DOI: Google ScholarDigital Library
- [9] . 2022. Folding left and right matters: Direct style, accumulators, and continuations. Journal of Functional Programming (2022).
To appear .Google Scholar - [10] . 2022. Getting there and back again. Fundamenta Informaticae 185, 2 (
May 2022), 115–183. Google ScholarDigital Library
- [11] . 2005. There and back again. Fundamenta Informaticae 66, 4 (2005), 397–413.
A preliminary version was presented at the 2002 ACM SIGPLAN International Conference on Functional Programming (ICFP 2002) .Google ScholarDigital Library
- [12] . 2009. Refunctionalization at work. Science of Computer Programming 74, 8 (2009), 534–549.Google Scholar
Digital Library
- [13] . 2000. Lambda-dropping: Transforming recursive equations into programs with block structure. Theoretical Computer Science 248, 1–2 (2000), 243–287.Google Scholar
Digital Library
- [14] . 1972. The humble programmer. Commun. ACM 15, 10 (
October 1972), 859–866.ACM Turing Award lecture .Google ScholarDigital Library
- [15] . 2007. Tools and libraries to model and manipulate circular programs. In Proceedings of the 2007 ACM SIGPLAN Symposium on Partial Evaluation and Semantics-Based Program Manipulation (PEPM 2007), and (Eds.). ACM Press, Nice, France, 102–111.Google Scholar
Digital Library
- [16] . 2013. Two puzzles from Danvy and Goldberg’s “There and back again”. Retrieved from http://toccata.lri.fr/gallery/there_and_back_again.en.html.Google Scholar
- [17] . 2016. ‘There and back again’ and what happened after. In Compose Conference ( http://www.composeconference.org/2016/program/). http://www.composeconference.org/2016/program/. https://www.youtube.com/watch?v=u_OsUlwkmBQ.Google Scholar
- [18] . 1999. Context-moving transformations for function verification. In Proceedings of the Logic Program Synthesis and Transformation (LOPSTR’99)(
Lecture Notes in Computer Science , Vol. 1817), (Ed.). Springer-Verlag, 293–312.Google Scholar - [19] . 2016. Deriving pure, naturally-recursive operations for processing tail-aligned lists. In Proceedings of the Scheme and Functional Programming Workshop (Co-located with ICFP 2016), and (Eds.). Nara, Japan. Retrieved from http://www.schemeworkshop.org/2016/.Google Scholar
- [20] . 1985. Lambda lifting: Transforming programs to recursive equations. In Proceedings of the Functional Programming Languages and Computer Architecture(
Lecture Notes in Computer Science , 201), (Ed.), Springer-Verlag, Nancy, France, 190–203.Google ScholarCross Ref
- [21] . 2019. Some Tricks for List Manipulation. Retrieved from https://doisinkidney.com/posts/2019-05-08-list-manipulation-tricks.html.Google Scholar
- [22] . 2020. Typing TABA. Retrieved from https://doisinkidney.com/posts/2020-02-15-taba.html.Google Scholar
- [23] . 1969. The Art of Computer Programming, Volume II: Seminumerical Algorithms. Addison-Wesley, Reading, MA.Google Scholar
- [24] . 1964. The mechanical evaluation of expressions. The Computer Journal 6, 4 (1964), 308–320.Google Scholar
Cross Ref
- [25] . 2012. Control-flow analysis of functional programs. ACM Computing Surveys 44, 3 (2012), 10:1–10:33. Google Scholar
Digital Library
- [26] . 2008. Some remarks on type systems for course-of-value recursion. In Proceedings of the Third Workshop on Logical and Semantic Frameworks with Applications (LSFA’08, Salvador, Brazil, August 26, 2008), Belo Horizonte, Elaine Pimentel, and Mario R. F. Benevides (Eds.). Electronic Notes in Theoretical Computer Science, Vol. 247, Elsevier, 103–121. Google Scholar
Digital Library
- [27] . 2022. Palindrome detection. (
January 2022). Retrieved from https://rosettacode.org/wiki/Palindrome_detection.Google Scholar - [28] . 2006. Swapping arguments and results of recursive functions. In Proceedings of the Mathematics of Program Construction, 8th International Conference, MPC 2006(
Lecture Notes in Computer Science , 4014), (Ed.). Springer, Kuressaare, Estonia, 379–396.Google ScholarDigital Library
- [29] . 2008. Langage de combinateurs pour XML: Conception, Typage, et Implantation. PhD thesis. LRI, Université Paris Sud, Orsay, France.
In English .Google Scholar - [30] . 2007. Lightweight fusion by fixed point promotion. In Proceedings of the Thirty-Fourth Annual ACM Symposium on Principles of Programming Languages (SIGPLAN Notices, Vol. 42, No. 1), (Ed.). ACM Press, Nice, France, 143–154.Google Scholar
Digital Library
- [31] . 1988. Importing and exporting information in program development. In Partial Evaluation and Mixed Computation, , , and (Eds.). North-Holland, 405–425.Google Scholar
- [32] . 1996. A comparative revisitation of some program transformation techniques. In Proceedings of the Partial Evaluation(
Lecture Notes in Computer Science , 1110), , , and (Eds.). Springer-Verlag, Dagstuhl, Germany, 355–385.Google ScholarCross Ref
- [33] . 1972. Definitional interpreters for higher-order programming languages. In Proceedings of 25th ACM National Conference. Boston, Massachusetts, 717–740.Google Scholar
Digital Library
- [34] . 1998. Definitional interpreters revisited. Higher-Order and Symbolic Computation 11, 4 (1998), 355–361.Google Scholar
Digital Library
- [35] . 2014. Programs and Proofs: Mechanizing Mathematics with Dependent Types. JetBrains/SPbSU Summer School. Retrieved from https://ilyasergey.net/pnp-2014/.Google Scholar
- [36] . 2006. Multi-return function call. Journal of Functional Programming 4–5, 16 (2006), 547–582.
A preliminary version was presented at the 2004 ACM SIGPLAN International Conference on Functional Programming (ICFP 2004) .Google ScholarDigital Library
- [37] 1984. Common Lisp: The Language. Digital Press.Google Scholar
- [38] . 1961. Handwritten Notes. (1961).
Archive of working papers and correspondence. Bodleian Library, Oxford, Catalogue no. MS. Eng. misc. b.267 .Google Scholar - [39] . 1974. Continuations: A Mathematical Semantics for Handling Full Jumps.
Technical Monograph PRG-11. Oxford University Computing Laboratory, Programming Research Group, Oxford, England.Google Scholar - [40] . 1976. SASL language manual.
Technical Report . St. Andrews University, Department of Computational Science,.Google Scholar - [41] . 1982. Recursion equations as a programming language. In Functional Programming and its Applications, , , and (Eds.). Cambridge University Press.Google Scholar
- [42] . 2000. Continuations revisited. Higher-Order and Symbolic Computation 13, 1/2 (2000), 131–133.Google Scholar
Digital Library
- [43] . 2018. Backpropagation with callbacks: Foundations for efficient and expressive differentiable programming. In Proceedings of the Advances in Neural Information Processing Systems 31, , , , , , and (Eds.). Curran Associates, Inc., 10180–10191.Google Scholar
- [44] . 2019. Demystifying differentiable programming: Shift/reset the penultimate backpropagator. Proceedings of the ACM on Programming Languages 3, ICFP (2019), 96:1–96:31.
DOI: Google ScholarDigital Library
Index Terms
The Tortoise and the Hare Algorithm for Finite Lists, Compositionally
Recommendations
There and Back Again
Program Transformation: Theoretical Foundations and Basic Techniques. Part 1We present a programming pattern where a recursive function defined over a data structure traverses another data structure at return time. The idea is that the recursive calls get us 'there' by traversing the first data structure and the returns get us '...
There and Back Again
Program Transformation: Theoretical Foundations and Basic Techniques. Part 1We present a programming pattern where a recursive function defined over a data structure traverses another data structure at return time. The idea is that the recursive calls get us 'there' by traversing the first data structure and the returns get us '...
Coinductive big-step operational semantics
Using a call-by-value functional language as an example, this article illustrates the use of coinductive definitions and proofs in big-step operational semantics, enabling it to describe diverging evaluations in addition to terminating evaluations. We ...

























































































Comments