PL: Lecture #14  Tuesday, October 25th

Alternative Church Encoding

Finally, note that this is just one way to encode things — other encodings are possible. One alternative encoding is in the following code — it uses a list of N falses as the encoding for N. This encoding makes it easier to add1 (just cons another #f), and to sub1 (simply cdr). The tradeoff is that some arithmetics operations becomes more complicated, for example, the definition of + requires the fixpoint combinator. (As expected, some people want to see what can we do with a language without recursion, so they don’t like jumping to Y too fast.)

;; An alternative "Church" encoding: use lists to encode numbers

#lang pl schlac

(define identity (lambda (x) x))

;; Booleans (same as before)

(define #t (lambda (x y) x))
(define #f (lambda (x y) y))

(define if (lambda (c t e) (c t e))) ; not really needed

(test (->bool (if #t #f #t))
      => '#f)
(test (->bool (if #f ((lambda (x) (x x)) (lambda (x) (x x))) #t))
      => '#t)

(define and (lambda (a b) (a b a)))
(define or  (lambda (a b) (a a b)))
(define not (lambda (a x y) (a y x)))

(test (->bool (and #f #f)) => '#f)
(test (->bool (and #t #f)) => '#f)
(test (->bool (and #f #t)) => '#f)
(test (->bool (and #t #t)) => '#t)
(test (->bool (or  #f #f)) => '#f)
(test (->bool (or  #t #f)) => '#t)
(test (->bool (or  #f #t)) => '#t)
(test (->bool (or  #t #t)) => '#t)
(test (->bool (not #f)) => '#t)
(test (->bool (not #t)) => '#f)

;; Lists (same as before)

(define cons (lambda (x y s) (s x y)))
(define car (lambda (x) (x #t)))
(define cdr (lambda (x) (x #f)))

(define 1st car)
(define 2nd (lambda (l) (car (cdr l))))
(define 3rd (lambda (l) (car (cdr (cdr l)))))
(define 4th (lambda (l) (car (cdr (cdr (cdr l))))))
(define 5th (lambda (l) (car (cdr (cdr (cdr (cdr l)))))))

(define null (lambda (s) #t))
(define null? (lambda (x) (x (lambda (x y) #f))))

;; Natural numbers (alternate encoding)

(define 0 identity)

(define add1 (lambda (n) (cons #f n)))

(define zero? car) ; tricky

(define sub1 cdr) ; this becomes very simple

;; Note that we could have used something more straightforward:
;; (define 0 null)
;; (define add1 (lambda (n) (cons #t n))) ; cons anything
;; (define zero? null?)
;; (define sub1 (lambda (l) (if (zero? l) l (cdr l))))

(define 1 (add1 0))
(define 2 (add1 1))
(define 3 (add1 2))
(define 4 (add1 3))
(define 5 (add1 4))

(test (->nat* (add1 (add1 5))) => '7)
(test (->nat* (sub1 (sub1 (add1 (add1 5))))) => '5)
(test (->bool (and (zero? 0) (not (zero? 3)))) => '#t)
(test (->bool (zero? (sub1 (sub1 (sub1 3))))) => '#t)

;; list-of-numbers tests
(define l123 (cons 1 (cons 2 (cons 3 null))))
(test (->listof ->nat* l123) => '(1 2 3))
(test (->listof (->listof ->nat*) (cons l123 (cons l123 null)))
      => '((1 2 3) (1 2 3)))

;; Recursive functions

(define Y
  (lambda (f)
    ((lambda (x) (x x)) (lambda (x) (f (x x))))))
(rewrite (define/rec f E) => (define f (Y (lambda (f) E))))

;; note that this example is doing something silly now
(define/rec length
  (lambda (l)
    (if (null? l)
      (add1 (length (cdr l))))))
(test (->nat* (length l123)) => '3)

;; addition becomes hard since it requires a recursive definition
;; (define/rec +
;;  (lambda (m n) (if (zero? n) m (+ (add1 m) (sub1 n)))))
;; (test (->nat* (+ 4 5)) => '9)

;; faster alternative:
(define/rec +
  (lambda (m n)
    (if (zero? m) n
        (if (zero? n) m
            (add1 (add1 (+ (sub1 m) (sub1 n))))))))
(test (->nat* (+ 4 5)) => '9)

;; subtraction is similar to addition
;; (define/rec -
;;  (lambda (m n) (if (zero? n) m (- (sub1 m) (sub1 n)))))
;; (test (->nat* (- (+ 4 5) 4)) => '5)
;; but this is not "natural subtraction": doesn't work when n>m,
;; because (sub1 0) does not return 0.

;; a solution is like alternative form of +:
(define/rec -
  (lambda (m n)
    (if (zero? m) 0
        (if (zero? n) m
            (- (sub1 m) (sub1 n))))))
(test (->nat* (- (+ 4 5) 4)) => '5)
(test (->nat* (- 2 5)) => '0)
;; alternatively, could change sub1 above:
;; (define sub1 (lambda (n) (if (zero? n) n (cdr n))))

;; we can do multiplication in a similar way
(define/rec *
  (lambda (m n)
    (if (zero? m) 0
        (+ n (* (sub1 m) n)))))
(test (->nat* (* 4 5)) => '20)
(test (->nat* (+ 4 (* (+ 2 5) 5))) => '39)

;; and the rest of the examples

(define/rec fact
  (lambda (x)
    (if (zero? x) 1 (* x (fact (sub1 x))))))
(test (->nat* (fact 5)) => '120)

(define/rec fib
  (lambda (x)
    (if (or (zero? x) (zero? (sub1 x)))
      (+ (fib (sub1 x)) (fib (sub1 (sub1 x)))))))
(test (->nat* (fib (* 5 2))) => '89)

;; Fully-expanded Fibonacci (note: much shorter than the previous
;; encoding, but see how Y appears twice -- two "((lambda" pairs)
(define fib((lambda(f)((lambda(x)(x x))(lambda(x)(f(x x)))))(lambda(
f)(lambda(x)(((((x(lambda(x y)x))(x(lambda(x y)x)))((x(lambda(x y)y)
)(lambda(x y)x)))(lambda(s)(s(lambda(x y)y)(lambda(x)x))))((((lambda
(f)((lambda(x)(x x))(lambda(x)(f(x x))))) (lambda(f)(lambda(m n)((m(
lambda(x y)x))n (((n(lambda(x y)x)) m)(lambda(s)((s (lambda(x y)y))(
lambda(s)((s (lambda(x y)y))((f(m(lambda(x y)y)))(n(lambda(x y)y))))
))))))))(f(x(lambda(x y)y))))(f((x(lambda(x y)y))(lambda(x y)y))))))

Implementing define-type & cases in Schlac

Another interesting way to implement lists follows the pattern matching approach, where both pairs and the null value are represented by a function that serves as a kind of a match dispatcher. This function takes in two inputs — if it is the representation of null then it will return the first input, and if it is a pair, then it will apply the second input on the two parts of the pair. This is implemented as follows (with type comments to make it clear):

;; null : List
(define null
  (lambda (n p)

;; cons : A List -> List
(define cons
  (lambda (x y)
    (lambda (n p)
      (p x y))))

This might seem awkward, but it follows the intended use of pairs and null as a match-like construct. Here is an example, with the equivalent Racket code on the side:

;; Sums up a list of numbers
(define (sum l)
  (l                    ; (match l
  0                    ;  ['() 0]
  (lambda (x xs)      ;  [(cons x xs)
    (+ x (sum xs)))))  ;    (+ x (sum xs))])

In fact, it’s easy to implement our selectors and predicate using this:

(define null? (lambda (l) (l #t (lambda (x xs) #f))))
(define car  (lambda (l) (l #f (lambda (x xs) x))))
(define cdr  (lambda (l) (l #f (lambda (x xs) xs))))
;; in the above `#f' is really any value, since it
;; should be an error alternatively:
(define car (lambda (l)
              (l ((lambda (x) (x x)) (lambda (x) (x x))) ; "error"
                (lambda (x y) x))))

The same approach can be used to define any kind of new data type in a way that looks like our own define-type definitions. For example, consider a much-simplified definition of the AE type we’ve seen early in the semester, and a matching eval definition as an example for using cases:

(define-type AE
  [Num Number]
  [Add AE AE])
(: eval : AE -> Number)
(define (eval expr)
  (cases expr
    [(Num n)  n]
    [(Add l r) (+ (eval l) (eval r))]))

We can follow the above approach now to write Schlac code that more than being equivalent, is also very similar in nature. Note that the type definition is replaced by two definitions for the two constructors:

(define Num (lambda (n)  (lambda (num add) (num n  ))))
(define Add (lambda (l r) (lambda (num add) (add l r))))
(define/rec eval
  (lambda (expr) ; `expr` is always a (lambda (num add) ...), and it
                ; expects a unary `num` argument and a binary `add`
    (expr (lambda (n)  n)
          (lambda (l r) (+ (eval l) (eval r))))))
(test (->nat (eval (Add (Num 1) (Num 2)))) => '3)

We can even take this further: the translations from define-type and cases are mechanical enough that we could implement them almost exactly via rewrites (there are a subtle change in that we’re list field names rather than types):

(rewrite (define-type -ignored-Type- [Variant arg ...] ...)
      => (define Variant
          (lambda (arg ...)
            (lambda (Variant ...) (Variant arg ...))))
(rewrite (cases value [(-ignored-Variant- arg ...) result] ...)
      => (value (lambda (arg ...) result) ...))

And using that, an evluator is simple:

(define-type AE [Num n] [Add l r] [Sub l r] [Mul l r])
(define/rec eval
  (lambda (expr)
    (cases expr
      [(Num n)  n]
      [(Add l r) (+ (eval l) (eval r))]
      [(Sub l r) (- (eval l) (eval r))]
      [(Mul l r) (* (eval l) (eval r))])))
(test (->nat (eval (Mul (Add (Num 1) (Num 2))
                        (Sub (Num 4) (Num 2)))))
      => '6)

Recursive Environments

PLAI §11.5

What we really need for recursion, is a special kind of an environment, one that can refer to itself. So instead of doing (note: calls removed for readability):

{with {fact {fun {n}
              {if {zero? n} 1 {* n {fact {- n 1}}}}}}
  {fact 5}}

which does not work for the usual reasons, we want to use some

{rec {fact {fun {n}
            {if {zero? n} 1 {* n {fact {- n 1}}}}}}
  {fact 5}}

that will do the necessary magic.

One way to achieve this is using the Y combinator as we have seen — a kind of a “constructor” for recursive functions. We can do that in a similar way to the rewrite rule that we have seen in Schlac — translate the above expression to:

{with {fact {make-rec
              {fun {fact}
                {fun {n}
                  {if {zero? n} 1 {* n {fact {- n 1}}}}}}}}
  {fact 5}}

or even:

{with {fact {{fun {f} {{fun {x} {f {x x}}} {fun {x} {f {x x}}}}}
            {fun {fact}
              {fun {n}
                {if {zero? n} 1 {* n {fact {- n 1}}}}}}}}
  {fact 5}}

Now, we will see how it can be used in our code to implement a recursive environment.

If we look at what with does in

{with {fact {fun {n}
              {if {zero? n} 1 {* n {call fact {- n 1}}}}}}
  {call fact 5}}

then we can say that to evaluate this expression, we evaluate the body expression in an extended environment that contains fact, even if a bogus one that is good for 0 only — the new environment is created with something like this:

extend("fact", make-fact-closure(), env)

so we can take this whole thing as an operation over env

add-fact(env) := extend("fact", make-fact-closure(), env)

This gives us the first-level fact. But fact itself is still undefined in env, so it cannot call itself. We can try this:


but that still doesn’t work, and it will never work no matter how far we go:


What we really want is infinity: a place where add-fact works and the result is the same as what we’ve started with — we want to create a “magical” environment that makes this possible:

let magic-env = ???
such that:
  add-fact(magic-env) = magic-env

which basically gives us the illusion of being at the infinity point. This magic-env thing is exactly the fixed-point of the add-fact operation. We can use:

magic-env = rec(add-fact)

and following the main property of the Y combinator, we know that:

magic-env = rec(add-fact)          ; def. of magic-env
          = add-fact(rec(add-fact)) ; Y(f) = f(Y(f))
          = add-fact(magic-env)    ; def. of magic-env

What does all this mean? It means that if we have a fixed-point operator at the level of the implementation of our environments, then we can use it to implement a recursive binder. In our case, this means that a fixpoint in Racket can be used to implement a recursive language. But we have that — Racket does have recursive functions, so we should be able to use that to implement our recursive binder.

There are two ways that make it possible to write recursive functions in Racket. One is to define a function, and use its name to do a recursive call — using the Racket formal rules, we can see that we said that we mark that we now know that a variable is bound to a value. This is essentially a side-effect — we modify what we know, which corresponds to modifying the global environment. The second way is a new form: letrec. This form is similar to let, except that the scope that is established includes the named expressions — it is exactly what we want rec to do. A third way is using recursive local definitions, but that is equivalent to using letrec, more on this soon.

Recursion: Racket’s letrec

So we want to add recursion to our language, practically. We already know that Racket makes it possible to write recursive functions, which is possible because of the way it implements its “global environment”: our evaluator can only extend an environment, while Racket modifies its global environment. This means that whenever a function is defined in the global environment, the resulting closure will have it as its environment “pointer”, but the global environment was not extended — it stays the same, and was just modified with one additional binding.

But Racket has another, a bit more organized way of using recursion: there is a special local-binding construct that is similar to let, but allows a function to refer to itself. It is called letrec:

(letrec ([fact (lambda (n)
                (if (zero? n)
                  (* n (fact (- n 1)))))])
  (fact 5))

Some people may remember that there was a third way for creating recursive functions: using local definition in function bodies. For example, we have seen things like:

(define (length list)
  (define (helper list len)
    (if (null? list)
      (helper (rest list) (+ len 1))))
  (helper list 0))

This looks like the same kind of environment magic that happens with a global define — but actually, Racket defines the meaning of internal definitions using letrec — so the above code is exactly the same as:

(define (length list)
  (letrec ([helper (lambda (list len)
                    (if (null? list)
                      (helper (rest list) (+ len 1))))])
    (helper list 0)))

The scoping rules for a letrec is that the scope of the bound name covers both the body and the named expression. Furthermore, multiple names can be bound to multiple expressions, and the scope of each name covers all named expression as well as the body. This makes it easy to define mutually recursive functions, such as:

(letrec ([even? (lambda (n) (if (zero? n) #t (odd?  (- n 1))))]
        [odd?  (lambda (n) (if (zero? n) #f (even? (- n 1))))])
  (even? 99))

But it is not a required functionality — it could be done with a single recursive binding that contains several functions:

(letrec ([even+odd
          (list (lambda (n)
                  (if (zero? n) #t ((second even+odd) (- n 1))))
                (lambda (n)
                  (if (zero? n) #f ((first  even+odd) (- n 1)))))])
  ((first even+odd) 99))

This is basically the same problem we face if we want to use the Y combinator for mutually recursive bindings. The above solution is inconvenient, but it can be improved using more lets to have easier name access. For example:

(letrec ([even+odd
          (list (lambda (n)
                  (let ([even? (first  even+odd)]
                        [odd?  (second even+odd)])
                    (if (zero? n) #t (odd? (- n 1)))))
                (lambda (n)
                  (let ([even? (first  even+odd)]
                        [odd?  (second even+odd)])
                    (if (zero? n) #f (even? (- n 1))))))])
  (let ([even? (first  even+odd)]
        [odd?  (second even+odd)])
    (even? 99)))