Recursion in the Lambda Calculus
Here's a simple recursive factorial function in Racket. (Note the use of the slightly longer
lambda notation, for reasons that will become clear later.)
(define fact (lambda (n) (if (equal? n 1) 1 (* n (fact (- n 1))))))
As natural as this might appear, this makes use of the ability of Racket to reference identifiers within their definitions, which of course is the heart of recursion as a programming technique. However, when we try to translate these recursive references into the lambda calculus, we run into some trouble. Because all function creation expressions use λ, these functions are not named, and so of course cannot be referenced within their bodies!
And yet it is possible to express recursion in the lambda calculus, without using self-referential identifiers. This is probably the most intricate technical topic presented in the course, so please do read through this article slowly, and make sure you run all of the code examples as you progress.
Moving the self-reference
The problem with the above definition is the recursive reference to
fact. Our first step will to create a higher-order function which "fills in" this function.
(define fact-maker (lambda (f) ; This is body of fact, except using the parameter f (lambda (n) (if (equal? n 1) 1 (* n (f (- n 1))))))) > (fact-maker fact) #<procedure> > ((fact-maker fact) 5) 120
Of course, this doesn't solve the problem: in order to achieve the factorial behaviour, we're still using the recursively-defined
fact! However, the above function calls yield a very interesting observation:
(fact-maker fact) is identical to
fact. We can substitute this into the definition:
(define fact-maker (lambda (f) (lambda (n) (if (equal? n 1) 1 ; We've replaced f by (fact-maker f) (* n ((fact-maker f) (- n 1))))))) > (fact-maker fact) #<procedure> > ((fact-maker fact) 5) 120
At this point you might be thinking that it looks like we've only made our lives more complicated, and indeed we have. However, consider what happens when we give the new
fact-maker a different argument:
(fact-maker "Hi") ; substitute "Hi" for f (lambda (n) (if (equal? n 1) 1 (* n ((fact-maker "Hi") (- n 1)))))
See it? This definition is exactly the same as the recursive definition of factorial! In other words:
> ((fact-maker "Hi") 5) 120
Even though this definition is still recursive, it's quite revealing: the argument to
fact-maker in the function body doesn't matter! Thus we can combine our previous two attempts to completely remove the recursive reference:
(define fact-maker (lambda (f) (lambda (n) (if (equal? n 1) 1 ; (f f) instead of (fact-maker f) (* n ((f f) (- n 1))))))) > ((fact-maker fact-maker) 5) 120
fact-maker to itself, we get
(fact-maker fact-maker) appearing within the body at line 7, and thus achieving the same "recursive" definition of
Note that this definition eliminates the syntactic recursive reference: a use of the identifier being defined inside the body of the function. However, we've kept the spirit of self-reference, as (unlike the previous attempts),
fact-maker will now only work when it is called with itself as an argument. However, this is perfectly acceptable in the lambda calculus: the expression (λx.x)(λx.x) is a simple example of this.
And that's about it for the factorial function: we've rewritten the standard recursive definition into a non-recursive one, using higher-order functions.
> (define factorial (fact-maker fact-maker)) > (factorial 5) 120
What about other recursive functions? Using the previous strategy, here is how you could define a non-recursive function that sums a list:
(define sum-maker (lambda (f) (lambda (lst) (if (empty? lst) 0 (+ (first lst) ((f f) (rest lst))))))) (define sum (sum-maker sum-maker)) > (sum '(1 2 3 4)) 10
As always, the repetition between our factorial and sum functions begs for abstraction. Notice that the only difference is the innermost function (lines 4-6 above). So we can simply abstract this part out into a separate function, though we must pass in the
(define (fact-template g n) (if (equal? n 1) 1 (* n (g (- n 1))))) (define fact-maker (lambda (f) (lambda (n) (fact-template (f f) n)))) > ((fact-maker fact-maker) 5) 120
Finally, we can make
fact-template an argument to abstract this technique.
; The two templates (define (fact-template g n) (if (equal? n 1) 1 (* n (g (- n 1))))) (define (sum-template g lst) (if (empty? lst) 0 (+ (first lst) (g (rest lst))))) ; Generic functions for this technique (define (rec-maker template) (lambda (f) (lambda (n) (template (f f) n)))) (define (Y template) ; maker abstracts the above fact-maker and sum-maker (let ([maker (rec-maker template)]) (maker maker))) (define fact (Y fact-template)) (define sum (Y sum-template))
This final function
Y, which takes the template and returns a recursive function based on that template, is often called the strict Y combinator (or sometimes Z combinator), and is a crown jewel of the lambda calculus. It also lends its name to the famed startup accelerator.
There is one special property of the Y combinator that is too interesting to pass up, especially since we've made it this far.
This function is an example of a fixed point combinator, and satisfies a truly amazing property. A definition: fixed point of a function
f is a value
x such that
(f x) == x. For example, two fixed points of the
sqr Racket function are 0 and 1, as
(sqr 0) == 0 and
(sqr 1) ==.
What does this have to do with the Y combinator? We'll show, using nothing more than simple substitution, that the Y combinator always returns a fixed point of its input, and this is (in spirit) how it "implements" recursion. More precisely, we'll prove that
(Y g) is a fixed point of
(g (Y g)) == (Y g) n.
(Y g) ; call Y (let ([maker (rec-maker g)]) (maker maker)) ; call rec-maker (let ([maker (lambda (f) (lambda (n) (g (f f) n)))]) (maker maker)) ; perform substitution into body of let ((lambda (f) (lambda (n) (g (f f) n))) (lambda (f) (lambda (n) (g (f f) n)))) ; Call the first function (sub in second expression for "f") (lambda (n) (g ((lambda (f) (lambda (n) (g (f f) n))) (lambda (f) (lambda (n) (g (f f) n)))) n)) ; Substituting previous equalities (lambda (n) (g (Y g) n)
This derivation shows that
(Y g) == (lambda (n) (g (Y g) n)), or put another way, for all
((Y g) n) == (g (Y g) n). This may not sound exactly like a fixed point as we defined above, and this is consequence of the fact that the Y combinator we derived is a bit more complex than the standard one, due to the fact that we require strict evaluation in Racket. However, after you've studied currying, you'll see that actually this statement really implies a true equality
(Y g) == (g (Y g))!