Recursion
Recursion is where the definition of something makes use of the thing being defined. It is most often met in the recursive definition of functions (methods, procedures, subroutines) and data-types but recursive definitions of data-structures [All89, All93] are possible in a suitable programming language. Mutual recursion is where the definitions of two or more things make use of each other.
- Examples:
- 0! = 1 | n! = n*(n-1)!, n>0 — factorial function
- type list e = nil | cons e (list e) — data-type
- length nil = 0 | length cons h t = 1+length t
- type tree e = emptyTree | fork (tree e) e (tree e)
- height emptyTree = 0 |
height fork L elt R = 1+max (height L) (height R)- posInts = cons 1 (map succ posInts) — data structure [1,2,3,...]
- 0! = 1 | n! = n*(n-1)!, n>0 — factorial function
Commonly, recursion is effected by referring to the name of the thing being defined but this is not essential, an alternative being to use the fixed-point operator Y, see below.
1. Linear Recursion
1.1. Procedures
Linear or unary recursion is where a definition makes one use of the thing being defined, as in the data-type 'list' and the function 'length' above. In general we have a procedure 'recP(.)'
proc recP(x) // a recursive procedure { if( isBC(x) ) BC(x); else { b(x); recP(f(x)); // recursion a(x); } }//recP(x)
where 'x' is one or more parameters, BC(x) is the action to take for a base case, isBC(x) recognises a base case, b(x) is something that happens before the recursive call and a(x) after, and f(x) acts on the parameter(s).
1.2. Tail recursion
In the special case that a(.) is absent (or does nothing) we have
proc tailrP1(x) { if( isBC(x) ) BC(x); else { b(x); tailrP1(f(x)); } }//tailrP1(x)
which is tail recursive in that the recursive call is the last thing that the procedure does; such recursion can be replaced by a loop:
proc iterP1(x) // equivalent to tailrP1 { while( ! isBC(x) ) { b(x); x = f(x); } BC(x); }//iterP1(x)
When a(x) is present and f(x) has an inverse, f_inv(x), we can write
proc tailrP2(x) { var n = 0; proc fst(x) { if(isBC(x)) { BC(x); snd(n,f_inv(x)); } else { b(x); n ++ ; fst(f(x)); // tail rec } }//fst(x) proc snd(n, x) { if( n==0 ) return; else { a(x); snd(n-1, f_inv(x)); // tail rec } }//snd(n,x) fst(x); // begin }//tailrP2(x)
and fst(x) and snd(n.x) are both tail recursive and can be rewritten as loops:
proc iterP2(x) // equivalent to tailrP2 { var n = 0; while(!isBC(x)) { b(x); x = f(x); n ++ ; } // assert isBC(x) BC(x); while( n > 0 ) { x = f_inv(x); a(x); n -- ; } }//iterP2(x)
1.3. Accumulating parameters
If f(x) does not have an inverse we have to collect, in an accumulating parameter, xs, the various values that x goes through so that b(.) can be applied to them in the reverse order to a(.).
proc tailrP3(x) { function fst(xs, x) // accumulate 'xs' { if( isBC(x) ) { BC(x); snd(xs.length-1, xs); } else { b(x); xs[xs.length] = x; // accumulate fst(xs, f(x)); // tail rec } }//fst(x) function snd(n, xs) // now use 'xs' { if( n < 0 ) return else { a(xs[n]); snd(n-1, xs); // tail rec } }//snd fst(new Array(), x); }//tailrP3(x)
now fst(.) and snd(.) are both tail recursive and so can be be rewritten as loops (exercise).
1.4. Functions
A fairly general template for a recursive function (a subroutine that returns a value as its result) is
function recF(x) // linear recursive function { return isBC(x) ? FBC(x) : g(recF(f(x))); }
where FBC is what to do in the function's base case, f is called before recF is called recursively and g is called afterwards. If g is absent we have tail recursion
function tailrF1(x) { return isBC(x) ? FBC(x) : tailrF1(f(x)); }//tailrF1
which can be rewritten using iteration
function iterF1(x) { while( ! isBC(x) ) x = f(x); return FBC(x); }//iterF1(x)
When g is present we might write
function tailrF2(x) { function trF(gs,x) //trF is a tail rec fn { return isBC(x) ? gs(FBC(x)) : trF(function(z){return g(gs(z));}, f(x)); }//trF return trF(id, x); }//tailrF2(x)
where trF is tail recursive and its parameter, gs, accumulates the composition of an appropriate number of calls to g. This can be rewritten as two loops, one for trf and one for gs:
function iterF2(x) { var n = 0; while( ! isBC(x) ) { x = f(x); n ++ ; } x = FBC(x); for( ; n > 0; n -- ) x = g(x); return x; }//iterF2(x)
2. Binary Recursion
Binary recursion is where a definition can make two recursive uses of the thing being defined, for example, in the definition of the data-type of binary trees, and of many functions on those binary trees. Some divide and conquer algorithms, such as merge sort and quick sort, are naturally binary recursive. Another example is generating sequences of n pairs of matched brackets "by choices".
3. General Recursion
A more general form of recursion is where a definition can make any number of recursive uses of the thing being defined. One application of this is to get the effect of an arbitrary number of nested loops:
proc loops(m, n) { var state = new Array(); proc act(d) { if ( d==m ) { for(var i = 0; i<m; i ++) print(" "+state[i]); println("."); } else for(state[d]=0; state[d]<n; state[d]++) act(d+1); }//act(d) act(0); }//loops(m,n)
act(d) textually refers to act(d+1) just once but this is inside a loop. By the time(s) that the depth d==m there are effectively that many nested loops in operation. (As it stands, loops(m,n) runs throgh all m-digit, base n+1, numerals.) Many combinatorial constraint satisfaction problems (CSP) (n-queens, Good sequences, ...) can be solved with this kind of template by adding appropriate tests to ensure that state[0..d) satisfies the constraints of the particular problem.
4. Y
Lastly, the fixed-point operator, Y, provides a way to define a recursive function without the body of the function textually referring to itself.
function Y(G) { const Ggg = function(g) { return function(n) { return G(g(g))(n); }; }; return Ggg(Ggg); }//Y(G) function F(f) { return function(n) { return n<=1 ? 1 : n*f(n-1); }; }//F(f)
Y(F)(n) = n!.
Y(F) is a fixed-point of F, that is,
F(Y F) = Y F.
Note that
Ggg does not textually refer to itself
although it is applied to itself within Y,
nor does F textually refer to itself {f is not F) and, e.g.,
F(function(m){return m+1;})(n) = n2,
and F(factorial)(n) = factorial(n):
Y F n = Ggg(Ggg)(n)
where Ggg=function(g) {return function(n)
{return F(g(g))(n);}}
= F(Ggg(Ggg))(n)
= n<=1 ? 1 : n*Ggg(Ggg)(n-1)
= n<=1 ? n! : n*(Y F (n-1)) — and induction...
= n!
The usual definition of Y in a
"lazy" functional
programming language is slightly different.
5. Tests
References
- [All89] L. Allison, 'Circular Programs and Self-Referential Structures',
Software Practice and Experience, 19(2), pp.99-109,
doi:10.1002/spe.4380190202,
1989.
- [All93] L. Allison, 'Applications of Recursively Defined Data Structures', Australian Computer Journal, 25(1) pp.14-20, arxiv:2206.12795, 1993.