Introduction

As we (may) know recursion is one of the most important elements in functional programming, and one of the charm of recursion, usually paired with algebraic data types is that the computation can be guaranteed to terminate by the compiler, if implemented in a specific way, a.k.a. the “Structural Recursion”.

For example:

data Tree = Leaf | Branch Tree Int Tree

treeSum :: Tree -> Int
treeSum Leaf = 0
treeSum (Branch l x r) = treeSum l + x + treeSum r

treeSum could be guaraneteed that it terminates regardless of the input (although haskell doesn’t do that by default), because the argument of the recursive call treeSum l and treeSum r is strictly smaller than the parameter, in the sense that l and r are the “sub-structure” of the input Branch l x r.

The structural recursion is even more important in theorem provers like Agda and Coq, and the structural condition is strictly enforced by those compilers, because otherwise we would be able to prove anything with an infinite loop.

(* Of course this would be rejected by Coq *)
Definition anything_is_true (A : Prop) : A := anything_is_true A.

However, the structural restriction is sometimes a bit too strong, even though sometimes it’s “easy” to tell that the structure of the arguments for the recursive call should be decreasing, consider this famous sorting algorithm:

quickSort :: Ord a => [a] -> [a]
quickSort [] = []
quickSort (x : xs) = quickSort l ++ [x] ++ quickSort r
  where
    l = filter (<= x) xs
    r = filter (>  x) xs

Of course haskell doesn’t argue with us about this recursive definition, but it should be clear that this is not a structural recursion, as l and r here are not necessarily structurally smaller than the argument x : xs. And language like “Coq” would mercilessly reject a similar definition. Although coq is being reasonable rejecting this definition, how do we convince the compiler that this is indeed a safe recursive call, given the filter is implemented properly. Years ago I implemented a similar thing in agda, and what I did is use a compiler pragma to bypass the structural termination check, which surely is not a very reliable way if we are going to implement something more complicated than this.

In these two (or three) articles, I’m going to share some of the interesting techniques I learned which enable us to implement less restricted recursions, without violating the structural recursion restriction.

For the sake of visual clarity I would use Idris (instead of Coq) as the sample langauge which has a similar syntax to Haskell.

Well-founded Recursion/Induction

Well-founded Relation and the Finitely Descending Chain

Our first technique, probably the most practical one we are going to explore , makes use of this set theory concept “Well-founded Relation”, frankly the mathematical definition looks quite scary for those unfamiliar with the notations. But the idea is, given a set \(S\) with a binary relation \(R\), and with an arbitrary element \({a~|~a \in S}\), we can find a chain with relation \(R\): \(R~a_0~a\), \(R~a_1~a_0\) , \(R~a_2~a_1\) and it goes on. If for any element \(a \in S\), this chain starts from \(a\) stops at some point, i.e. \(R~a_{n+1}~a_n\) , but there does NOT exist \(\{m~|~m \in S \land R~m~a_{n+1} \}\), then we say the relation is well-founded. For example, the “less-than” (\(<\)) relation on natural number set is well-founded, because for any arbitrarily chosen number \(n\) and an arbitrarily chosen “less-than chain”, the chain always stops at 0, because there isn’t any natural number that is less than 0.

Why is this useful? Well, not immediately, but it gives us a tangible approach to convince someone who doesn’t trust intuition (the compilers) that some recursive call terminates: if we are given a natural number x as argument for some recursive function f, then as long as we guarantee the argument of all the recursive calls are less than x, we guarantee the recursion terminates, because “less-than” is a well-founded relation, and as long as we follows the “chain” when making recursive calls, the chain has to terminate at some point to respect the mathematical truth.

And we can conceptualize the “chain” with something like this:

data ChainOf : (a -> a -> Type) -> a -> Type where
  Stop : (x : a) -> ((y : a) -> Not (DPair a (\y => r y x))) -> ChainOf r x
  GoOn : (x : a) -> (y : a) -> r y x -> ChainOf r y -> ChainOf r x

f : ChainOf r a -> ()
f (Stop x p) = ()                    -- do something with base case
f (GoOn x y p subChain) = f subChain -- do something between x and y

And we can safely define a recursive function with respect to the structure of the chain constrained by the relation r, breaking free from having to recurse on the structure of the exact value (y doesn’t have to be a sub-structure of x, yet the recursive call shift the argument from something about x to something about y).

However this definition of ChainOf isn’t that practically useful, since we have no control over the value of the sub-case of recursion, we are given y and a proof that r y x holds, but when defining recursive function like quickSort, what we want is a specific value of argument for sub-cases (from x : xs to filter (<=) xs, for example), an arbitrary pre-selected choice wouldn’t do.

Acc and its Recursion Operator

So instead of recursing on a specific chain, we need a collection of chains from which we can select the ones that suits our need. In most of the cases the potential choices could be infinite, so we could do something like this:

-- Acc is short for "Accessible"
data Acc : (a -> a -> Type) -> a -> Type where
  MkAcc : (x : a) -> ((y : a) -> r y x -> Acc r y) -> Acc r x

Here Acc r x reads: x is accessible (from the end of the chain) using relation r. The function (y : a) -> r y x -> Acc r y gives us a way to select the next step of recursion, but of course we cannot just select an arbitrary value in the set (of type a), we need to provide a proof that the y we choose indeed satisfies r y x. So this simple, eh, probably not, this minimal definition captures all the possible chains from the end of the chain to Acc r x.

But wait, how this single constructor expresses where the chain stops? The chain stops when there isn’t any y : a such that r y x, so the selection pool for “next steps” is practically empty as we wouldn’t be able to access any next step Acc r y if we cannot prove any r y x.

And for those of you who are familiar with inductive/recursive principles, the recursive principle for this type, or the fixpoint operator we are looking for is:

accRec : {P : a -> Type}
      -> ((x : a) -> ((y : a) -> r y x -> P y) -> P x)
      -> {x : a} -> Acc r x -> P x
accRec rec (MkAcc x f) = rec x $ \y, ryx => accRec rec (f y ryx)

From the perspective of the definition of this operator, ignoring the the longest parameter type, the type of the function says: if x is accessible with a certain r, when you give me something (the parameter with the longest type), I can help you run the recursive procedure and give you the final result (P x). And the longest parameter type says: for all the possible x, if you can show me how to compute the result for all y which is the next step of the chain, I can show give you the result for x.

Then the definition is rather short since we only have one case to deal with, the key is to show the recursive procedure (rec) that, starting from an arbitrary sub-case (y : a with r y x), how to compute the result about x (P x), which is indeed possible because the accessibility condition (Acc r x) helps us prove that any sub-case y is accessible given r y x. Then the recursive call of accRec handles the rest.

Before delving into the important details of this recursive definition, let’s quickly formulate the “well-founded” condition, and show you how they are used:

wellFounded : (r : a -> a -> Type) -> Type
wellFounded {a} r = (x : a) -> Acc r x

-- simply delegates to accRec
-- as `wellFounded` says any value of `a` is accessible
wfRec : {P : a -> Type}
     -> ((x : a) -> ((y : a) -> r y x -> P y) -> P x)
     -> wellFounded r -> (x : a) -> P x
wfRec rec wf x = accRec rec (wf x)

Well-founded Recursion and Revisiting Quicksort

Now let’s go back to the quicksort and think about what are we doing with the list. The well-founded relation we can find here, is that the argument lists for the recursive call is guaranteed to be shorter than the the original argument. And we know that the length of finite lists can only decrease finitely, hence a comparison on length of the two lists is a well-founded relation.

So first we are going to define a relation about the lengths of two lists, and convince our compiler that the relation is indeed well-founded.

LengthLT : {a : Type} -> List a -> List a -> Type
LengthLT xs ys = LT (length xs) (length ys)

wfLengthLT : {a : Type} -> wellFounded (LengthLT {a})
wfLengthLt = ...

I omit the proofs here, but if you are interested you can find them here

Then the implementation for quicksort follows:

quickSort : Ord a => List a -> List a
quickSort {a} = wfRec {P = const (List a)} quickSort' wfLengthLT
  where
    quickSort' : (xs : List a)
              -> ((ys : List a) -> LengthLT ys xs -> List a)
              -> List a
    quickSort' [] _ = []
    quickSort' (x :: xs) rec
      =  rec (filter (\v => v <= x) xs) (LTESucc (filterSmaller _))
      ++ [x]
      ++ rec (filter (\v => v > x) xs) (LTESucc (filterSmaller _))

Unlike the complex-looking types of the fixpoint operator and quicksort itself, the actual definition is rather clean. We delegate the implementation of quicksort to a application of the well-foundedness recursive operator wfRec. Like using a conventional fixpoint operator, we accept the recursive version of the function as a parameter (rec), but here the recursive call is guarded by the condition that the argument have to be shorter than the argument of parent call (LengthLT ys xs). And we have to provide the proof of such relation holds upon making the recursive call (LTESucc (filterSmaller _)). In this case we guarantee that the whatever comes out of the filter must not be longer than whatever goes in (idris generously provide exactly what we need in the library (filterSmaller)), hence the argument of the recursive call is at least shorter by one than the argument of the caller. Then the rest is only that you convincing yourself calling the rec is indeed making a recursive call.

Although it looks like we are doing exactly the same thing as in the intro, this definition perfectly conforms the structural restriction, hence is guaranteed by the compiler that it terminates with all possible inputs. Quite impressive isn’t it?

The Mystery of Structural Recursion

Apparently we achieve all these with the conventional structural recurison, but if you looking carefully at the definition of our essential operator:

accRec : {P : a -> Type}
      -> ((x : a) -> ((y : a) -> r y x -> P y) -> P x)
      -> {x : a} -> Acc r x -> P x
accRec rec (MkAcc x f) = rec x $ \y, ryx => accRec rec (f y ryx)

The argument of the recursive call is f y ryx. How is it a sub-structure of MkAcc x f when it’s a return value of a function call? And more importantly this definition doesn’t look terminating when there is only one case, and it’s a recursive case.

Well, it definitely should terminate as that’s what mathematics tells us. The most straightforward way to look at it is that it’s an generalized inductive type (or reflexive data type) that the function represents a mapping from an generalized index to a sub-structure. But probably easier to understand, another way is realizing that the function is not arbitrary, it comes from a correctly constructed value of inductive types, which is obligated to find a function that correctly construct the “sub-structure” whose procedure has to be finite (with the structural restriction enforced). So in the end, it will go down to the end of the chain, where any further descend will lead to a contradiction. For example when constructing (Acc (<) 0), it’s impossible to find any natural number a such that a < 0, hence the definition of (a : Nat) -> a < 0 -> Acc (<) a is trivial.

That’s also how a (seemingly) single case of recursion covers both base case and recursive case. When reaching the end of the chain, where there is no y such that r y x, r y x is isomorphic to \(\bot\) (a type with no value), so whatever we do when constructing (y : a) -> r y x -> P y is trivial since it’s impossible to find its arguments and the recursion will never be called.