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.
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 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 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 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!
Basis.
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.
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:
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.