skip to main content
research-article
Open Access

The Tortoise and the Hare Algorithm for Finite Lists, Compositionally

Published:03 March 2023Publication History

Skip Abstract Section

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”.

Skip 1BACKGROUND AND INTRODUCTION Section

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.

Fig. 1. Dear reader, part 1/2.

Fig. 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 List.hd and List.tl, respectively, yield the head and the tail of this list, and given an empty list, they raise an error. Since Gallina features only total functions, partial functions such as List.hd and List.tl need to be encoded, e.g., with an option type:

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, e1 && e2 is syntactic sugar for if e1 then e2 else false to achieve short-circuit evaluation, and both let foo x y = e and let foo x = fun y -> e are syntactic sugar for

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 visit is defined with let rec visit fp off sp \(=, \ldots ,\) the two expressions

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.

Skip 2IMPLEMENTING THE TORTOISE-AND-HARE ALGORITHM Section

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 and v2s, applying this function to vs yields v2s if and only if there exists a list v1s that has the same length as v2s and is such that vs is the result of List.append v1s v2s or of List.append v1s (m :: v2s), for some list element m.

In that light, the unit-test function above is more tellingly written as follows (@ is OCaml’s infix notation for List.append):

In order to traverse the list only once, and as per the tortoise-and-hare algorithm, let us use a fast pointer (fp) and a slow pointer (sp) that lags behind at half the distance from the beginning of the given list. Initially, both fp and sp point to the given list:

In words, fp denotes a suffix of the list denoted by sp—initially the given list, and a strict suffix after the first recursive call:

When processing a list (third clause), visit traverses this list two elements at a time with the fast pointer and one element at a time with the slow pointer. In this third clause, List.tl can safely be used for the slow pointer since fp denotes a suffix of the list denoted by sp and this suffix contains at least two elements.

The given list has an even length if visit eventually reaches the empty list (first clause). Then, there is no middle element, and sp points to the second half of the given list, which is returned.

The given list has an odd length if visit eventually reaches a singleton list (second clause). Then, there is a middle element, and sp points to the suffix of the given list that starts with this element. The tail of this suffix is the second half of the given list, which is returned. In this second clause, List.hd and List.tl can safely be used for the slow pointer since fp denotes a suffix of the list denoted by sp and this suffix contains 1 element.

Let us analyze second_half2:

This implementation is iterative: given a list of length 2n or \(2n+1\), it performs \(n+1\) tail calls to visit (the initial tail call and n tail-recursive calls).

This implementation is structurally recursive in that visit is called recursively on a strict suffix of the list denoted by fp. It is, however, not compositional in that its pattern of recursion does not follow the pattern of induction in the construction of lists. In other words, second_half2 cannot be expressed as an instance of list_fold_right:

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, and v2s, applying this function to vs yields (v1s, v2s) if and only if v1s and v2s have the same length and are such that vs is the result of List.append (List.rev v1s) v2s or of List.append (List.rev v1s) (m :: v2s), for some list element m.

In a nutshell, we revisit Section 2.1, also parameterizing visit with the reversed prefix accumulated so far (initially the empty list), and returning both this reversed prefix and the second half:

In words, fp denotes a suffix of the list denoted by sp (initially the given list, with an empty reversed prefix), as in second_half2. So the three clauses satisfy the same conditions as in second_half2, plus the condition that pfx_op denotes the reversed prefix of the given list up to the list denoted by sp:

In the third clause, every progress for the slow pointer so far (i.e., List.tl sp) is paired with an incremental construction of the reversed prefix so far (i.e., List.hd sp :: pfx_op). And as in second_half2, both List.hd and List.tl can safely be used.

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 second_half2, List.tl can safely be used.

Like second_half2, this implementation is iterative in that it is tail recursive, but it is not compositional and thus cannot be expressed as an instance of list_fold_right.

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:

List.hd and List.tl are safe because the two arguments have the same length. Also, the comparison stops as soon as two list elements differ, thanks to OCaml’s short-circuit evaluation of Boolean conjunctions, as was mentioned in Section 1.2. Otherwise, the comparison continues tail recursively.

Besides replacing compare_halves_naive by compare_halves_perspicuous in the definition of pal2_naive, we can also inline the call to reversed_first_half_and_second_half2 and relocate its context, using Ohori and Sasano’s lightweight fusion by fixed-point promotion [30], as reviewed in Appendix C:

The body of pal2_fused is a clone of the body of reversed_first_half_and_second_half2 where the perspicuous comparison function is tail-called in the two first clauses.

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 pal2_fused, instead of constructing the reversed prefix in order to deconstruct it in compare_halves_perspicuous, we might as well construct a function that performs this comparison, starting with a constant function that returns true. And then in the first and second clause, instead of applying compare_halves_perspicuous to the reversed prefix of the first half of the given list and to its second half, we would apply this function to this second half.

Replacing the reversed prefix and compare_halves_perspicuous by a function is not an ad-hoc transformation: it is the converse of Reynolds’s defunctionalization [33, 34], as reviewed in Appendix D. Indeed the very fact that the reversed prefix is solely constructed in order to be compared with the second half of the given list means that pal2_fused is in defunctionalized form. Also, since pal2_fused and compare_halves_perspicuous are tail recursive, the function that replaces the reversed prefix and compare_halves_perspicuous is a continuation [39, 42].

That the reversed prefix and compare_halves_perspicuous are the defunctionalized representation of a continuation is more visible in the following equivalent definition of the perspicuous comparison function. In this equivalent definition, an if-expression is used rather than a match-expression to dispatch on the reversed prefix, and this if-expression is commuted with the function abstraction that declares sfx, as foreshadowed in Section 1.2:

And indeed:

The empty list in the initial call to visit and the base case of compare_halves_perspicuous correspond to the initial continuation

Given (1) a slow pointer sp and a reversed prefix pfx_op and (2) the corresponding continuation k, the construction of the reversed prefix in the induction step of visit and its consumption in the induction step of compare_halves_perspicuous correspond to the new continuation

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 false.

Here is the refunctionalized version of pal2_fused. The relation between compare_halves_perspicuous and pfx_op (in the definition of pal2_fused) and k (in the definition of pal2_c) is that where k is applied to a list here, compare_halves_perspicuous pfx_op is applied to this list there:

Let us analyze pal2_c:

The auxiliary function visit is nearly in continuation-passing style, with the proviso that the continuation is affine [2], i.e., either is used once or is not used at all, a tell-tale sign of a control effect.

If the initial continuation is applied, the given list is a palindrome and the result is true; otherwise a continuation was not applied, the computation was discontinued, and the result is false: the given list is not a palindrome.

As in Sections 2.1 and 2.2, pal2_c is not compositional and thus cannot be expressed as an instance of list_fold_right.

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 visit (the initial call and n recursive calls), and then at most with \(n + 1\) tail calls to a continuation (the last one to the initial continuation) and with n comparisons.

The accompanying .ml file contains a traced version of pal2_c to visualize the computation.

2.3.4 Back to Direct Style.

The definition of pal2_c can be expressed in direct style, using the exception Nyet to account for the discontinuity:

The following analysis of pal2_d is similar to the analysis of pal2_c in Section 2.3.3:

The auxiliary function visit is in ordinary direct style, with the proviso that an exception is raised if two list elements differ to discontinue the computation.

If the initial call to visit completes, the given list is a palindrome and the result is true; otherwise the exception Nyet was raised, is caught, and the result is false: the given list is not a palindrome.

Like pal2_c, pal2_d is not compositional and thus cannot be expressed as an instance of list_fold_right.

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 visit (the initial call and n non-tail recursive calls), and then at most with \(n+2\) returns from visit (the initial return, n returns that correspond to the n non-tail recursive calls, and the final return) and with n comparisons.

The accompanying .ml file contains a traced version of pal2_d to visualize the computation.

2.3.5 A Depiction.

The resulting two implementations, pal2_c and pal2_d, coincide with the TABA-based palindrome detector presented in the early 2000’s [11, Section 6]. Here is a low-tech depiction of their computation, using ASCII art:

  • Initial call:

    The horizontal bar represents the given list, and its middle is marked with m. In the initial call to visit, both fp and sp 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 than sp.

  • Last call:

    In the last call to visit, fp points to the end of the given list and sp points to its middle.

  • First return (i.e., first application of the continuation):

    In the first return from visit, both sp and sfx 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 by sp and sfx 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 and sfx 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 fp and the “slow suffix” of the given list denoted by sp. These suffixes only coincide in the initial call to visit. In all the recursive calls, the fast suffix is a strict suffix of the slow suffix. Unfolding the initial call to visit promotes this property into an invariant:

The accompanying .ml file contains all the implementations presented in this section with all the initial calls to visit unfolded.

Skip 3IMPLEMENTING THE TORTOISE-AND-HARE ALGORITHM, COMPOSITIONALLY Section

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 list_fold_right.

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 visit to traverse the list one element at a time, not two. So let us define visit inductively over the fast pointer. If the fast pointer is to traverse the list one element at a time, the slow pointer should traverse it one element every second time. To this end, let us equip visit with a Boolean, off, to regulate whether the slow pointer should skip an element or not. This Boolean reflects whether the fast pointer is twice as far as the slow pointer or is off by 1:

In words, fp still denotes a suffix of the list denoted by sp—initially the given list, and a strict suffix after the first recursive call. The Boolean is initialized with false to convey that, initially, fp is (vacuously) twice as far as sp in the given list:

The given list has an even length if visit eventually reaches the empty list (first clause) with off denoting false. Otherwise, it has an odd length, fp denotes a strict suffix of the list denoted by sp, and it is safe to use List.tl.

When processing a list (second clause), visit traverses this list one element at a time with the fast pointer. It also flips the Boolean parameter. Depending on this Boolean, it skips one element with the slow pointer or it does not. In this second clause, List.tl can safely be used for the slow pointer since fp denotes a suffix of the list denoted by sp and this suffix contains at least one element.

Let us analyze second_half:

This implementation is iterative: given a list of length n, it performs \(n + 1\) tail calls to visit (the initial tail call and n tail-recursive calls). In second_half, the given list is traversed one element at a time with the fast pointer and one element every second time with the slow pointer, as regulated by the Boolean parameter, whereas in second_half2, the given list is traversed two elements at a time with the fast pointer and one element at a time with the slow pointer—a simple illustration of Blum’s speedup theorem for lists [7].

This implementation is compositional in that its pattern of recursion follows the pattern of induction in the construction of lists. In other words, second_half can be expressed as an instance of list_fold_right, using higher-order functions (see Appendix A):

Being tail recursive, this implementation can also be expressed as an instance of list_fold_left:

The alert reader will have noticed that in second_half_right and in second_half_left, the arguments of list_fold_right and the arguments of list_fold_left are the same. The equivalence of these two definitions is a corollary of Bird and Wadler’s second duality theorem [4, p. 68], as described in Appendix A and formalized in the accompanying .v file.

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 visit with the reversed prefix accumulated so far (initially the empty list), pairing the slow pointer with this reversed prefix since they both are regulated by off, and returning both this reversed prefix and the second half in the end:

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 off denotes true), the middle element is skipped); and

in the induction step, every progress for the slow pointer so far (i.e., List.tl sp) is paired with an incremental construction of the reversed prefix so far (i.e., List.hd sp :: pfx_op).

Like second_half, this implementation is compositional and so it can be expressed as an instance of list_fold_right (and list_fold_left).

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 reversed_first_half_and_second_half in the definition of pal_perspicuous and relocate its context, using Ohori and Sasano’s lightweight fusion:

As in Section 2.3.2, the body of pal_fused is a clone of the body of reversed_first_half_and_second_half where compare_halves_perspicuous is tail-called in the base case, the effect of lightweight fusion by fixed-point promotion. Here too, the reversed prefix is solely constructed in order to be compared with the second half of the given list, and so this implementation is in defunctionalized form too – the reversed prefix and compare_halves_perspicuous are the defunctionalized representation of a continuation:

For cosmetic value, let us curry visit and commute its tail-recursive call and the conditional expression, as foreshadowed in Section 1.2:

Let us analyze pal_c:

The auxiliary function visit is nearly in continuation-passing style, with the proviso that the continuation is affine (i.e., either used once or not used at all).

If the initial continuation is applied, the given list is a palindrome and the result is true; otherwise a continuation was not applied and the result is false: the given list is not a palindrome.

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 visit and the if-expression makes it clear that in the consequent call, the continuation grows, and that in the alternative call, it remains the same.

The accompanying .ml file contains a traced version of pal_c to visualize the computation.

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 pal_d is similar to the analysis of pal_c just above:

The auxiliary function visit is in ordinary direct style, with the proviso that an exception is raised if two list elements differ.

If the initial call to visit completes, the given list is a palindrome and the result is true; otherwise the exception Nyet was raised, is caught, and the result is false: the given list is not a palindrome.

This implementation is compositional and so it can be expressed as an instance of a fold function for lists.

The continuation is traded for recursive calls.

This implementation also has the hallmark of TABA in that it traverses the given list at call time and its second half at return time.

Having commuted the call to visit and the if-expression earlier on, the consequent recursive call is a non-tail call (its result is named) and the alternative recursive call is a tail call:

Any given list of length \(2 \cdot n\) is traversed with \(2 \cdot n + 1\) calls to visit (the initial call, n tail-recursive calls, and n non-tail recursive calls), and then at most with \(n+2\) returns from visit (the initial return, n returns that correspond to the n non-tail recursive calls, and the final return) and with n comparisons.

Any given list of length \(2 \cdot n + 1\) is traversed with \(2 \cdot n + 2\) calls (the initial call, \(n+1\) tail-recursive calls, and n non-tail recursive calls), and then at most with \(n+2\) returns from visit (the first return, n returns that correspond to the n non-tail recursive calls, and the final return) and with n comparisons.

The accompanying .ml file contains a traced version of pal_d to visualize the computation.

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 visit can be unfolded to ensure that when visit is called recursively, fp points to a strict suffix of the list sp points to:

The accompanying .ml file contains all the implementations presented in this section with all the initial calls to visit unfolded.

Skip 4REASONING ABOUT THE TORTOISE-AND-HARE ALGORITHM Section

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 vs_minus_sp be the prefix of the given list (vs) up to the slow pointer (sp) and sp_minus_fp be the prefix of a suffix of the given list from the slow pointer up to the fast pointer (fp), the fast pointer is twice as far as the slow pointer in the given list whenever vs_minus_sp and sp_minus_fp have the same length:

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 vs_minus_sp:

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 List.hd and List.tl, which are partial functions. The Gallina implementation uses list_hd and list_tl (see Section 1.2), where partiality is encoded with an option type. This encoding has a ripple effect: the result of second_half2 needs to be made optional too, and so this specification comes with a proof obligation to the effect that second_half2 never yields None:

The accompanying .v file also contains a proof that at most one function satisfies this specification as well as a proof of the soundness of the unit tests from Section 2.1, namely: a function that satisfies the specification passes the unit tests. (Completeness is out of reach [14].)

Inlining the definitions of list_hd and list_tl from Section 1.2, here is the implementation of second_half2 in Gallina:

For simplicity, the auxiliary function is not local, as in Section 2.1, but is declared globally. (In technical terms, second_half2_aux is the lambda-lifted counterpart of visit, as reviewed in Appendix B.)

The following lemma captures the intent of second_half2_aux:

where ++ is an infix notation for list concatenation. In words:

The alignment premiss of the lemma is that since the fast pointer, fp, is twice as fast as the slow pointer, sp, it is is twice as far in the given list, vs.

With this premiss, applying second_half2_aux to the fast suffix denoted by fp and to the slow suffix denoted by sp yields Some sfx, where sfx denotes the right half of the given list.

This lemma also captures that second_half2_aux is total in that None is never returned: it was indeed safe to use List.hd and List.tl in Section 2.1.

That second_half2 satisfies specification_of_second_half follows as a corollary.

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 .v file and used it to prove Lemma about_second_half2_aux:

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 list_ind2, and proof that the main function satisfies the specification as a corollary of the lemma. For brevity, we defer to the accompanying .v file here.

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 .v file for the others.

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 pal2_d and two nested option types in pal2_d_aux. As in Section 4.1, the option type in pal2_d and the outer option type in pal2_d_aux are a ripple effect of encoding List.hd and List.tl, which are partial functions, as total functions. The inner option type in pal2_d_aux conveys that the result is either a suffix that needs to continue to be traversed, or that the given list is not a palindrome:

In the definition of pal2_d, the result of the initial call to pal2_d_aux is verified to be the empty list, as in Section 2.3.4.

The following lemma captures the intent of pal2_d_aux:

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 pal2_d_aux to the fast suffix denoted by fp and to the slow suffix denoted by sp either yields the double injection of a suffix of the slow suffix, sfx, that (a) has the same length as the prefix of the slow suffix and (b) is such that as per the depiction in Section 2.3.5, the prefix of the slow suffix up to sfx is a palindrome; or this application yields Some None and the given list is not a palindrome.

This lemma also captures that pal2_d_aux is total in that None is never returned: It was indeed safe to use List.hd and List.tl when implementing pal2_d in Section 2.3.4.

This lemma is proved by induction on the given list, using list_ind2. The correctness of pal2_d follows as a corollary, and so does its totality: None is never returned.

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.

Skip 5REASONING ABOUT THE TORTOISE-AND-HARE ALGORITHM, COMPOSITIONALLY Section

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 vs_minus_sp, the prefix of the given list up to the slow pointer, and with sp_minus_fp, the prefix of a suffix of the given list from the slow pointer up to the fast pointer. To account for the asynchronous progression of the slow pointer, we also quantify it with a regulating Boolean, off. If off denotes false, the slow pointer is in sync with the fast pointer and vs_minus_sp and sp_minus_fp have the same length; and if off denotes true, the slow pointer is not in sync with the fast pointer and vs_minus_sp is shorter than sp_minus_fp:

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 vs_minus_sp:

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 List.hd and List.tl are encoded with an option type. So likewise, we implement second_half with an option type:

The following lemma captures the intent of second_half_aux:

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 second_half_aux to the fast suffix, a regulating Boolean, and the slow suffix yields Some sfx, where sfx denotes the right half of the given list.

This lemma also captures that second_half_aux is total in that None is never returned: it was indeed safe to use List.hd and List.tl in Section 3.1.

That second_half satisfies specification_of_second_half follows as a corollary.

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 .v file here.

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 .v file for the others.

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 list_hd and list_tl as total functions:

The following lemma captures the intent of pal_c_aux:

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 k, applying pal_c_aux to the fast suffix, a regulating Boolean, the slow suffix, and the continuation either gives rise to k being applied to a suffix sfx that (a) has the same length as the prefix of the slow suffix and (b) is such that as per the depiction in Section 2.3.5, the prefix of the slow suffix up to sfx is a palindrome; or this application yields Some false and the given list is not a palindrome.

This lemma also captures that pal_c_aux is total in that None is never returned: It was indeed safe to use List.hd and List.tl when implementing pal_c in Section 3.3.

This lemma is proved by induction on the given list. Thanks to the decomposition of this list into vs_minus_sp ++ sp_minus_fp ++ fp, the proof only uses equational reasoning, not relational reasoning, even though pal_c_aux involves a continuation.

The correctness of pal_c follows as a corollary, and so does its totality.

5.3.2 Direct Style.

The codomain of pal_d_aux needs two option types: The outer one is used for totality, and the inner one is used to convey the progress in the given list or that this list is not a palindrome, in which case None is incrementally returned (this incremental return is short-circuited in the continuation-passing version—the continuation is not applied—and in the OCaml direct-style version—an exception is raised):

The following lemma captures the intent of pal_d_aux:

The situation is much the same as for Lemma about_pal_c_aux:

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 pal_d_aux to the fast suffix, a regulating Boolean, and the slow suffix either yields the double injection of a suffix of the slow suffix, sfx, that (a) has the same length as the prefix of the slow suffix and (b) is such that as per the depiction in Section 2.3.5, the prefix of the slow suffix up to sfx is a palindrome; or this application yields Some None and the given list is not a palindrome.

This lemma also captures that pal_d_aux is total in that None is never returned: It was indeed safe to use List.hd and List.tl when implementing pal_d in Section 3.3.

This lemma is proved by induction on the given list, using equational reasoning.

Again, the correctness of pal_d follows as a corollary, and so does its totality.

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.

Skip 6THERE AND FORTH AGAIN Section

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 yields true if and only if there exists a list ws which is such that vs is the result of List.append ws ws or of List.append ws (m :: ws), for some list element m.

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 pal2_c, there is only one free variable in the continuation in lap2_c, namely k. So defunctionalizing it yields a data type that is isomorphic to Peano numbers, not lists. (When the given list is completely traversed, this Peano number represents half of the length of this list.) Also, the iteration of successive returns can be independently carried out with tail calls, which is characteristic of the “There and Forth Again” (TAFA) tail-recursion pattern:

In words:

there traverses the given list at double speed with a fast and slow pointer, tail recursively, and then

forth traverses the right half of the given list and its first half, comparing their successive elements at each step and discontinuing the traversal as soon as one of the comparisons fails.

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.

Skip 7CONCLUSION AND PERSPECTIVES Section

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 (list_fold_right, p. 6, which Strachey named R0) and of structural tail recursion over lists using an accumulator (list_fold_left, p. 13, which Strachey named R1). He pointed out that instantiating list_fold_right with nil (the empty list) and cons (the list constructor) yields the list-copy function, and that instantiating list_fold_left with nil and cons yields the list-reverse function. To wit, in OCaml:

And indeed applying list_fold_right to n, c, and 1 :: 2 :: 3 :: [] homomorphically gives rise to

whereas applying list_fold_left to n, c, and 1 :: 2 :: 3 :: [] homomorphically gives rise to

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 list_fold_left to make it more manifest that list_fold_right associates to the right and list_fold_left associates to the left when the second argument of the fold functions is written in infix form. And indeed applying list_fold_right to a, \((\oplus)\), and \([1; 2; 3]\) gives rise to

\(\begin{equation*} 1 \oplus (2 \oplus (3 \oplus a)), \end{equation*}\)

whereas applying list_fold_left to a, \((\oplus)\), and \([1; 2; 3]\) gives rise to

\(\begin{equation*} ((a \oplus 1) \oplus 2) \oplus 3, \end{equation*}\)

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 List.fold_right and List.fold_left do not have the same type:

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 list_fold_left and list_fold_right have the same type, Bird and Wadler’s second duality theorem [4, p. 68] is a lot simpler to state and to prove (see the accompanying .v file):

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 second_half_right to second_half_left on p. 13—which again is a lot simpler.

So for example, since fun _ ih -> succ ih is left permutative, life is simpler with the following two equivalent definitions of the length function for lists:

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, list_last on p. 4 is expressed with the following second-order instance of list_fold_right, where a function is accumulated and then applied to v:

A similar higher-order scheme makes it possible to express list_fold_left and list_fold_right as instances of each other [9, Section 1.7].

That said, left permutativity is infrequent: in the definition just above, fun v’ ih v => ih v’ is not left permutative. More noticeably, List.cons is not left permutative, hence Strachey’s observation that folding left and right with nil and cons do not give the same result. Small causes, big effects:

B LAMBDA LIFTING AND LAMBDA DROPPING

In Section 2.1, the global definitions of list_fold_right and list_fold_left make use of block structure and lexical scope: each auxiliary function visit is declared locally (block structure) and uses nil_case and cons_case, which are declared outside its local declaration (lexical scope). Alternatively, list_fold_left and list_fold_right could be implemented globally as recursive equations [41], i.e., as mutually recursive functions without block structure, i.e., without local declarations of auxiliary functions.

For another example, the following function maprev is implemented with two recursive equations. Given a function and list, it maps this function to each element of this list, iteratively accumulates their result onto a list, and eventually returns this list:

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 maprev reads as follows:

In this lambda-dropped version, the auxiliary version (traditionally named visit) is only parameterized with the arguments it works on (i.e., vs and a, not f). Also, it is guaranteed to be only called with the empty list as the initial value of the accumulator.

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, visit is tail recursive in that its recursive call is a tail call. In the candidate for lightweight fusion, both the lexical block for visit and the initial call to visit are not in tail position, and therefore the initial call to visit is not a tail call: visit eventually returns its accumulator, which is then passed to g, the result of which is then passed to f. In the candidate for lightweight fission, both the lexical block for visit and the initial call to visit are in tail position, and therefore the initial call to visit is a tail call: its accumulator is eventually passed to g, the result of which is then passed to f.

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.

Skip D.1An Example of Defunctionalization Section

D.1 An Example of Defunctionalization

Consider the continuation-passing implementation from Section 3.3:

The continuation has type 'a list -> bool. This type is inhabited by instances of two function abstractions:

one in the initial call to visit: fun sfx -> assert (sfx = []); true, and

one in the body of visit: fun sfx -> assert (sfx != []); List.hd sfx = List.hd sp && k (...).

The resulting functional values are applied in the nil case and in the cons case of visit.

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, sp and k. Therefore, the first constructor has no argument and the second has two:

The corresponding defunctionalized program reads as follows:

The vigilant reader will have noticed that since dispatch_cont returns a function, the defunctionalized program is still higher-order. However, its two invocations are fully applied and so the (higher-order) dispatch function is better defined as a (first-order) continue function with two arguments:

And since the type cont is isomorphic to that of lists, that is how the defunctionalized continuation is represented, which leads one to the uncurried definition of pal_fused in Section 3.3, where continue is named compare_halves_perspicuous.

Skip D.2Refunctionalization Section

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 pal2_fused is refunctionalized into pal2_refunct and Section 3.3 how pal_fused is refunctionalized into pal_refunct.

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 walk traverses the given list and grows a continuation to apply to the given list (and to an empty list of pairs) in order to traverse it again (and accumulate the resulting list of pairs). Perhaps more picturesquely, the calls create a spring of continuations which is then iteratively unwound as these continuations are successively applied. The idea of TABA is to traverse another data structure during this iteration.

This solution can be expressed in direct style as follows:

This implementation also has the hallmark of TABA in that moonwalk traverses the given list at call time and the given list again at return time (while accumulating the resulting list of pairs). Perhaps more picturesquely, the calls create a spring of returns which is then iteratively unwound as these returns successively take place. The idea of TABA is to traverse another data structure during this iteration.

As in Sections 2 and 3, the uses of List.hd and List.tl are safe, and indeed these implementations are correct [10].

REFERENCES

  1. [1] Amin Nada and Rompf Tiark. 2018. Collapsing towers of interpreters. Proceedings of the ACM on Programming Languages 2, POPL (2018), 52:1–52:33. DOI:Google ScholarGoogle ScholarDigital LibraryDigital Library
  2. [2] Berdine Josh. 2004. Linear and Affine Typing of Continuation-Passing Style. Ph. D. Dissertation. Queen Mary, University of London.Google ScholarGoogle Scholar
  3. [3] Bertot Yves and Castéran Pierre. 2004. Interactive Theorem Proving and Program Development. Springer.Google ScholarGoogle ScholarDigital LibraryDigital Library
  4. [4] Bird Richard and Wadler Philip. 1988. Introduction to Functional Programming. Prentice-Hall International.Google ScholarGoogle ScholarDigital LibraryDigital Library
  5. [5] Bird Richard S.. 1984. Using circular programs to eliminate multiple traversals of data. Acta Informatica 21 (1984), 239250. Google ScholarGoogle ScholarDigital LibraryDigital Library
  6. [6] Bird Richard S.. 1986. An Introduction to the Theory of Lists. Technical Monograph PRG-56. Oxford University, Computing Laboratory, Oxford, England.Google ScholarGoogle Scholar
  7. [7] Blum Manuel. 1967. A machine-independent theory of the complexity of recursive functions. Journal of the ACM 14, 2 (1967), 322336.Google ScholarGoogle ScholarDigital LibraryDigital Library
  8. [8] Brunel Aloïs, Mazza Damiano, and Pagani Michele. 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 ScholarGoogle ScholarDigital LibraryDigital Library
  9. [9] Danvy Olivier. 2022. Folding left and right matters: Direct style, accumulators, and continuations. Journal of Functional Programming (2022). To appear.Google ScholarGoogle Scholar
  10. [10] Danvy Olivier. 2022. Getting there and back again. Fundamenta Informaticae 185, 2 (May2022), 115–183. Google ScholarGoogle ScholarDigital LibraryDigital Library
  11. [11] Danvy Olivier and Goldberg Mayer. 2005. There and back again. Fundamenta Informaticae 66, 4 (2005), 397413. A preliminary version was presented at the 2002 ACM SIGPLAN International Conference on Functional Programming (ICFP 2002).Google ScholarGoogle ScholarDigital LibraryDigital Library
  12. [12] Danvy Olivier and Millikin Kevin. 2009. Refunctionalization at work. Science of Computer Programming 74, 8 (2009), 534549.Google ScholarGoogle ScholarDigital LibraryDigital Library
  13. [13] Danvy Olivier and Schultz Ulrik P.. 2000. Lambda-dropping: Transforming recursive equations into programs with block structure. Theoretical Computer Science 248, 1–2 (2000), 243287.Google ScholarGoogle ScholarDigital LibraryDigital Library
  14. [14] Dijkstra Edsger W.. 1972. The humble programmer. Commun. ACM 15, 10 (October1972), 859866. ACM Turing Award lecture.Google ScholarGoogle ScholarDigital LibraryDigital Library
  15. [15] Fernandes João Paulo and Saraiva João. 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), Ramalingam Ganesan and Visser Eelco (Eds.). ACM Press, Nice, France, 102111.Google ScholarGoogle ScholarDigital LibraryDigital Library
  16. [16] Filliâtre Jean-Christophe. 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 ScholarGoogle Scholar
  17. [17] Foner Kenneth. 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 ScholarGoogle Scholar
  18. [18] Giesl Jürgen. 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), Bossi Annalisa (Ed.). Springer-Verlag, 293312.Google ScholarGoogle Scholar
  19. [19] Hemann Jason and Friedman Daniel P.. 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), Gray Kathy and Shinn Alex (Eds.). Nara, Japan. Retrieved from http://www.schemeworkshop.org/2016/.Google ScholarGoogle Scholar
  20. [20] Johnsson Thomas. 1985. Lambda lifting: Transforming programs to recursive equations. In Proceedings of the Functional Programming Languages and Computer Architecture(Lecture Notes in Computer Science, 201), Jouannaud Jean-Pierre (Ed.), Springer-Verlag, Nancy, France, 190203.Google ScholarGoogle ScholarCross RefCross Ref
  21. [21] Kidney Donnacha Oisín. 2019. Some Tricks for List Manipulation. Retrieved from https://doisinkidney.com/posts/2019-05-08-list-manipulation-tricks.html.Google ScholarGoogle Scholar
  22. [22] Kidney Donnacha Oisín. 2020. Typing TABA. Retrieved from https://doisinkidney.com/posts/2020-02-15-taba.html.Google ScholarGoogle Scholar
  23. [23] Knuth Donald E.. 1969. The Art of Computer Programming, Volume II: Seminumerical Algorithms. Addison-Wesley, Reading, MA.Google ScholarGoogle Scholar
  24. [24] Landin Peter J.. 1964. The mechanical evaluation of expressions. The Computer Journal 6, 4 (1964), 308320.Google ScholarGoogle ScholarCross RefCross Ref
  25. [25] Midtgaard Jan. 2012. Control-flow analysis of functional programs. ACM Computing Surveys 44, 3 (2012), 10:1–10:33. Google ScholarGoogle ScholarDigital LibraryDigital Library
  26. [26] Miranda-Perea Favio E.. 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 ScholarGoogle ScholarDigital LibraryDigital Library
  27. [27] Mol Mike. 2022. Palindrome detection. (January2022). Retrieved from https://rosettacode.org/wiki/Palindrome_detection.Google ScholarGoogle Scholar
  28. [28] Morihatao Akimasa, Kakehi Kazuhiko, Hu Zhenjiang, and Takeichi Masato. 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), Uustalu Tarmo (Ed.). Springer, Kuressaare, Estonia, 379396.Google ScholarGoogle ScholarDigital LibraryDigital Library
  29. [29] Nguyen Kim. 2008. Langage de combinateurs pour XML: Conception, Typage, et Implantation. PhD thesis. LRI, Université Paris Sud, Orsay, France. In English.Google ScholarGoogle Scholar
  30. [30] Ohori Atsushi and Sasano Isao. 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), Felleisen Matthias (Ed.). ACM Press, Nice, France, 143154.Google ScholarGoogle ScholarDigital LibraryDigital Library
  31. [31] Pettorossi Alberto and Proietti Maurizio. 1988. Importing and exporting information in program development. In Partial Evaluation and Mixed Computation, Bjørner Dines, Ershov Andrei P., and Jones Neil D. (Eds.). North-Holland, 405425.Google ScholarGoogle Scholar
  32. [32] Pettorossi Alberto and Proietti Maurizio. 1996. A comparative revisitation of some program transformation techniques. In Proceedings of the Partial Evaluation(Lecture Notes in Computer Science, 1110), Danvy Olivier, Glück Robert, and Thiemann Peter (Eds.). Springer-Verlag, Dagstuhl, Germany, 355385.Google ScholarGoogle ScholarCross RefCross Ref
  33. [33] Reynolds John C.. 1972. Definitional interpreters for higher-order programming languages. In Proceedings of 25th ACM National Conference. Boston, Massachusetts, 717740.Google ScholarGoogle ScholarDigital LibraryDigital Library
  34. [34] Reynolds John C.. 1998. Definitional interpreters revisited. Higher-Order and Symbolic Computation 11, 4 (1998), 355361.Google ScholarGoogle ScholarDigital LibraryDigital Library
  35. [35] Sergey Ilya. 2014. Programs and Proofs: Mechanizing Mathematics with Dependent Types. JetBrains/SPbSU Summer School. Retrieved from https://ilyasergey.net/pnp-2014/.Google ScholarGoogle Scholar
  36. [36] Shivers Olin and Fisher David. 2006. Multi-return function call. Journal of Functional Programming 4–5, 16 (2006), 547582. A preliminary version was presented at the 2004 ACM SIGPLAN International Conference on Functional Programming (ICFP 2004).Google ScholarGoogle ScholarDigital LibraryDigital Library
  37. [37] Jr. Guy L. Steele1984. Common Lisp: The Language. Digital Press.Google ScholarGoogle Scholar
  38. [38] Strachey Christopher. 1961. Handwritten Notes. (1961). Archive of working papers and correspondence. Bodleian Library, Oxford, Catalogue no. MS. Eng. misc. b.267.Google ScholarGoogle Scholar
  39. [39] Strachey Christopher and Wadsworth Christopher P.. 1974. Continuations: A Mathematical Semantics for Handling Full Jumps. Technical Monograph PRG-11. Oxford University Computing Laboratory, Programming Research Group, Oxford, England.Google ScholarGoogle Scholar
  40. [40] Turner David A.. 1976. SASL language manual. Technical Report. St. Andrews University, Department of Computational Science,.Google ScholarGoogle Scholar
  41. [41] Turner David A.. 1982. Recursion equations as a programming language. In Functional Programming and its Applications, Darlington John, Henderson Peter, and Turner David A. (Eds.). Cambridge University Press.Google ScholarGoogle Scholar
  42. [42] Wadsworth Christopher P.. 2000. Continuations revisited. Higher-Order and Symbolic Computation 13, 1/2 (2000), 131133.Google ScholarGoogle ScholarDigital LibraryDigital Library
  43. [43] Wang Fei, Decker James, Wu Xilun, Essertel Gregory, and Rompf Tiark. 2018. Backpropagation with callbacks: Foundations for efficient and expressive differentiable programming. In Proceedings of the Advances in Neural Information Processing Systems 31, Bengio S., Wallach H., Larochelle H., Grauman K., Cesa-Bianchi N., and Garnett R. (Eds.). Curran Associates, Inc., 1018010191.Google ScholarGoogle Scholar
  44. [44] Wang Fei, Zheng Daniel, Decker James M., Wu Xilun, Essertel Grégory M., and Rompf Tiark. 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 ScholarGoogle ScholarDigital LibraryDigital Library

Index Terms

  1. The Tortoise and the Hare Algorithm for Finite Lists, Compositionally

    Recommendations

    Comments

    Login options

    Check if you have access through your login credentials or your institution to get full access on this article.

    Sign in

    Full Access

    • Article Metrics

      • Downloads (Last 12 months)1,472
      • Downloads (Last 6 weeks)168

      Other Metrics

    PDF Format

    View or Download as a PDF file.

    PDF

    eReader

    View online with eReader.

    eReader

    HTML Format

    View this article in HTML Format .

    View HTML Format
    About Cookies On This Site

    We use cookies to ensure that we give you the best experience on our website.

    Learn more

    Got it!