Deriving Recursion from First Principles

Deriving Recursion from First Principles

2 Oct 2023

Or: Approaching the Y Combinator

These are some of my class notes. Learning to derive the Y Combinator from first principles is something I’ve always wanted to do. This isn’t quite the Y Combinator, but it’s very close and it still gets you recursion without relying on recursive structures to begin with.

In the beginning, we write a recursive function to compute the length of a list:

(let* ([len (λ (lst)
             (if (null? lst)
                 0
                 (+ 1 (len (cdr lst)))))])
  (len '(1 2 3)))

The let* syntax allows us to create local variable bindings that can reference themselves. But let’s suppose we don’t have let*—what do we do?

We can make a function that we give to itself. That then returns the function we want, with the outer function in scope. So, the outer function len in this example has “type”1 $self → Int → Int. That makes it clear that to get the Int → Int function we want, we have to pass the function to itself.

(let ([len (λ (len)
             (λ (lst)
               (if (null? lst)
                   0
                   (+ 1 ((len len) (cdr lst))))))])
  ((len len) '(1 2 3)))

But it’d be nice if we could pull out that (len len) in the body of the function. Let’s call that len1.

(let ([len (λ (len)
             (let ([len1 (λ (l) ((len len) l))])
               (λ (lst)
                 (if (null? lst)
                     0
                     (+ 1 (len1 (cdr lst)))))
               ))])
  ((len len) '(1 2 3)))

Note that what we can’t do is this:

(let ([len (λ (len)
             (let ([len1 (len len)])    ; problem here!
               (λ (lst)
                 (if (null? lst)
                     0
                     (+ 1 (len1 (cdr lst)))))
               ))])
  ((len len) '(1 2 3)))

Because this is an eager language, so that’d loop forever.

Let’s do that same trick for the outer len len:

(let ([len (λ (len)
             (let ([len1 (λ (l) ((len len) l))])
               (λ (lst)
                 (if (null? lst)
                     0
                     (+ 1 (len1 (cdr lst)))))
               ))])
  (let ([len1 (len len)])               ; type of len1 is Int → Int
    (len1 '(1 2 3))))

Note that the (len len) at the end of that is OK because we’re not doing another self application.

Let’s change that inner let to a λ just for fun.

(let ([len (λ (len)
             ((λ (len1)
                (λ (lst)
                  (if (null? lst) 0 (+ 1 (len1 (cdr lst))))))
              (λ (l) ((len len) l))))])
  ((λ (len1)
     (len1 '(1 2 3)))
   (len len)))

Rename lenself, len1rec, and just return the function without calling it:

(let ([self (λ (self)
              ((λ (rec)
                 (λ (lst)
                   (if (null? lst) 0 (+ 1 (rec (cdr lst))))))
               (λ (x) ((self self) x))))])
  (self self))

Ok, we are ready… let’s go ahead an extract that part (λ (lst) ...): that’s the only part of this that knows anything about computing the length of lists. We’ll wrap the whole thing in a function called mk_rec which takes a function of two arguments: the first argument we will pass rec to, and the second is the actual argument to the function.

(define (mk_rec fn_x) ; takes two args: the recursive thingy and the argument
  (let ([self (λ (self)
                ((λ (rec)
                   (λ (x) (fn_x rec x)))
                 (λ (x) ((self self) x))))])
    (self self)))

(define len (mk_rec (λ (rec lst) (if (null? lst) 0 (+ 1 (rec (cdr lst)))))))
(len '(1 2 3))                          ; returns 3

(define fact (mk_rec (λ (rec x) (if (zero? x) 1 (* x (rec (- x 1)))))))
(fact 5)                                ; returns 120

Note that that isn’t actually the the Y combinator exactly. But hey we have recursion without using recursion!


Just for fun:

Note that we could have defined let as:

(let ((let '`(let ((let ',let))
               ,let)))
  `(let ((let ',let))
     ,let))

Source: FreeBSD fortune files; this is probably my favorite quine ever.


  1. Scare quotes intentional: you can’t have self-referential types in the simply-typed lambda calculus like you see here. The STLC is called strongly normalizing, which means every well-typed term reduces to a value of that type. Thus, you can’t have something like the Y Combinator or Omega (((λ (x) (x x)) (λ (y) (y y)))) because these could loop forever (diverge). ↩︎

Mastodon