Fixpoints and Recursion

Recursive definitions require self-reference.  A recursive function in OCaml such as the factorial function,

let rec fact x = if x = 0 then 1 else x * fact (x - 1)

must be able to call itself recursively.  We cannot do this in the λ-calculus directly, because there are no names—all functions are anonymous.  In OCaml, we can fake this in the environment model using refs to create cycles:

let fact =
  let fact' : (int -> int) ref = ref (fun x -> x) in
  let f = fun x -> if x = 0 then 1 else x * (!fact' (x - 1)) in
  fact' := f;
  fun x -> !fact' x

But this still does not help, since the λ-calculus has no refs either, it is purely functional.  How then can we possibly define recursive functions in the λ-calculus without names or refs?  It seems hopeless.  However, believe it or not, it can be done.

Approximation of Recursive Functions

To illustrate, let's use the factorial function as an example.  Using our encoding of natural numbers as Church numerals developed in the last lecture, we would like to get a λ-term fact such that

fact  =  λ.(if-then-else (isZero n) 1 (mul n (fact (sub1 n))))

First, note that fact is a kind of limit of an inductively-defined sequence of functions factn, n >= 0, each of which can be defined without recursion.

fact0   =  λn.n
factn+1  =  λn.(if-then-else (isZero n) 1 (mul n (factn (sub1 n))))

Thus for any Church numeral m,

fact0 m  =>  m
fact1 m  =>  if-then-else (isZero m) 1 (mul m (fact0 (sub1 m)))
fact2 m  =>  if-then-else (isZero m) 1 (mul m (fact1 (sub1 m)))
fact3 m  =>  if-then-else (isZero m) 1 (mul m (fact2 (sub1 m)))
factn m  =>  if-then-else (isZero m) 1 (mul m (factn-1 (sub1 m)))

In this definition, we have not used names in any essential way, just as an abbreviation for something that can be defined purely functionally.  For example, fact2 is equivalent to

λn.(if-then-else (isZero n) 1
   (mul n ((λn.(if-then-else (isZero n) 1
               (mul n ((λn.n)(sub1 n)))))(sub1 n))))

which reduces via the substitution model (β-reduction) to

λn.(if-then-else (isZero n) 1
   (mul n (if-then-else (isZero (sub1 n)) 1
          (mul (sub1 n) (sub1 (sub1 n))))))

By limit we mean that the functions factn approximate fact more and more accurately as n gets larger, in the sense that they agree with fact on more and more inputs.  The first approximant fact0 agrees with fact on no inputs at all.  The second approximant fact1 agrees with fact on one input, namely 0.  The third approximant fact2 agrees with fact on two inputs, namely 0 and 1, and so on.  One can show by induction that factn agrees with fact on inputs 0, 1, ..., n-1.  Although none of these approximants are equal to fact, they get closer and closer as n gets larger.


Note that the inductive step in the inductive definition of factn+1 from factn can be expressed abstractly as a higher-order function.  If we define

t_fact  =  λF.λn.(if-then-else (isZero n) 1 (mul n (F (sub1 n))))

then our inductive definition of factn can be rewritten

fact0   =  fun x -> x
factn+1 =  t_fact factn

The real factorial function fact (if it exists!) should satisfy the equation

fact  =  λn.(if-then-else (isZero n) 1 (mul n (fact (sub1 n))))

In other words, it should be a fixpoint of t_fact:

fact  =  t_fact fact

Think of t_fact as the operation of "unwinding" the definition of fact once.  So if we had a general way of obtaining fixpoints in the λ-calculus, we might apply it to obtain a fixpoint of t_fact and this might do the trick.

Fixpoint theorems abound in mathematics.  A whorl on your head where your hair grows straight up is a fixpoint.  At any instant of time, there must be at least one spot on the globe where the wind is not blowing.  For any continuous map f from the closed real unit interval [0,1] to itself, there is always a point x such that f(x) = x.

The λ-calculus is no exception.  It turns out that any λ-term W has a fixpoint.  Consider the lambda term

λx.W(xx) λx.W(xx)

This is a fixpoint of W, as can be seen by performing one β-reduction step:

λx.W(xx) λx.W(xx)  =>  W (λx.W(xx) λx.W(xx))

Moreover, there is a lambda term Y, called the fixpoint combinator, which when applied to any W gives a fixpoint of W:

Y  =  λw.(λx.w(xx) λx.w(xx))

If we apply Y to t_fact, what do we get?  Define

fact  =  Y t_fact  =  λx.t_fact(xx) λx.t_fact(xx)

We know that this is a fixpoint of t_fact, i.e.

fact  =>  t_fact fact

Now we show by induction that this is indeed the factorial function; that is, for any n,

fact n  =>  n!


fact 0  =>  t_fact fact 0
  =>  λn.(if-then-else (isZero n) 1 (mul n (fact (sub1 n)))) 0
  =>  if-then-else (isZero 0) 1 (mul 0 (fact (sub1 0)))
  =>  1
  =>  0!

Induction step:

fact n+1  =>  t_fact fact n+1
  =>  λn.(if-then-else (isZero n) 1 (mul n (fact (sub1 n)))) n+1
  =>  if-then-else (isZero n+1) 1 (mul n+1 (fact (sub1 n+1)))
  =>  mul n+1 (fact (sub1 n+1))
  =>  mul n+1 (fact n)
  =>  mul n+1 n!     (by the induction hypothesis)
  =>  (n+1)!

Note that nowhere in our development did we use names for anything but abbreviations for anonymous functions.

Encoding in OCaml

We would like to encode Church numerals and recursion without names to illustrate these constructions in OCaml.  However, there are two immediate impediments to this project:

  1. OCaml is typed, and the lambda calculus is not.  Many of the traditional encodings given by Church do not typecheck in OCaml, even with polymorphic typing.
  2. OCaml is eager, but the encoding of recursion using the traditional fixpoint combinator Y yields looping behavior if evaluated using an eager reduction strategy.  Only lazy reduction yields normal forms.

It turns out that both these problems can be circumvented with a little extra work.

For the first, observe that some of the definitions we have given typecheck in OCaml and some do not.  The fixpoint combinator Y definitely does not typecheck:

# fun w -> (fun x -> w (x x)) (fun x -> w (x x));;
Error: This expression has type 'a -> 'b
       but an expression was expected of type 'a

The problem here is the subexpression (x x), which tries to apply x as a function to itself.  The type inference algorithm discovers a circularity when it tries to unify the polymorphic type of x as a function 'a -> 'b with the type of x as its own input 'a.  A similar situation arises when trying to apply a Church numeral n to a function on Church numerals such as in the definition of add.  There is no type s in OCaml with the property that s = s -> t.  However, there is something almost as good: a type s such that s = Fix (s -> t):

# type 'a fix = Fix of ('a fix -> 'a);;
type 'a fix = Fix of ('a fix -> 'a)

Note there is no base case to the inductive definition!  Nevertheless, we can construct objects of this type:

# Fix (fun _ -> 3110);;                                                
- : int fix = Fix 

Moreover, we can use such an object either as a function of type int fix -> int (provided we deconstruct it first using pattern matching to get rid of the Fix) or as an input to such a function.

Using the same idea, we can give appropriate recursive types for Church numerals:

type church = Ch of ((church -> church) -> church -> church)

For the Church numerals, the only thing we have to remember is to deconstruct it before applying it as a function.  For example, instead of

let add1 n = fun f -> fun x -> f (n f x)
let zero = fun f -> fun x -> x

which is the direct translation of Church's encoding, we take

let add1 (Ch n) = Ch (fun f -> fun x -> f (n f x))
let zero = Ch (fun f -> fun x -> x)

The second problem is simulating lazy evaluation.  Note that the if-then-else in the definition of fact must be evaluated lazily, otherwise the else clause will be evaluated prematurely.  However, our lambda calculus definition of if-then-else is evaluated eagerly when we run it in OCaml.  It will keep unwinding the definition of fact, trying to calculate better and better approximations before ever applying them, and this will go on forever.  To prevent this, we use thunks.  We wrap the then and else expressions in the body of a function to inhibit evaluation until the test has been evaluated, then evaluate the correct alternative to get the value.

Here is the encoding.  Give it a try!

Turn on Javascript to see the program.