Lecture 7: Lambda Calculus II

Goals for today:

  • Re-encounter substitution in the lambda calculus
  • See the basics of Church-encoding and desugaring into the lambda calculus
  • Implement the lambda calculus with environments
    • Understand closures

Logistics:

  • I’ve continued to reflow the schedule based on our pace; please check it
  • Quiz this Thurs/Fri

Lambda calculus

  • A brief note: the interpreter we worked through together in class last time had a bug in it (though the notes are correct); we will discuss this in more detail today
  • A brief note on references: Types and Programming Languages by Pierce has a wonderful chapter on the λ-calculus.
  • Substitution is the heart of the lambda calculus, so we should get some more practice with it.
  • Recall our simple lambda calculus syntax, which we will extend with numbers for the purposes of illustration:
<e> ::= (lambda (<id>) <e>)  ; λ-abstraction
      | (<e> <e>)            ; application
      | <id>                 ; variable
      | num
  • There is an alternative and popular syntax for the lambda calculus that you may see, which we will use for some examples here:
<e> ::= (λ <id> . <e>)
      | <id>
      | (<e> <e>)
  • Recall our notation: e1[x |-> e2] substitutes x for e2 in e1
  • Some examples of how substitution works in the lambda calculus:
; a simple example
(λ x . y)[y |-> 10] = (λ x. 10)

; an example involving shadowing
(λ x. y)[x |-> 10] = (λ x. y)

; substituting a lambda into a lambda
(λ x. y)[y |-> (λ y. y)] = (λ x. (λ y. y))

; substituting under application
((λ x. x) y)[y |-> 20] = ((λ x. x) 10)
  • With this rule for substitution, we can practice running some lambda terms by hand:
; example 1
((λ x. x) (λ y. y))
--> x[x |-> (λ y. y)]
--> (λ y. y)

; example 2
((λ y. (λ x. y)) 10)
--> (λ x. y)[y |-> 10]
--> (λ x. 10)

; example 3
(λ x. (λ x. x) 10)
--> (λ x. x)[x |-> 10]
--> (λ x. x)
  • Some in-class exercises: evaluate the following lambda terms
    • ((λ x. (λ y. x) 10)
    • ((λ x. (λ y. y)) (λ x . 10))
    • ((λ x. ((λ x . x) x)) (λ y . y))
  • Transitioning back to our implementation, we can now implement our lambda calculus interpreter, extended with numbers:
; interpreter for lambda calculus with numbers
(define-type LExp
  [varE (s : Symbol)]
  [numE (n : Number)]
  [lamE (x : Symbol) (body : LExp)]
  [appE (e : LExp) (arg : LExp)])

(define-type Value
  [funV (arg : Symbol) (body : LExp)]
  [numV (n : Number)])

(define (get-arg v)
  (type-case Value v
    [(funV arg body) arg]
    [else (error 'runtime "invalid")]))

(define (get-body v)
  (type-case Value v
    [(funV arg body) body]
    [else (error 'runtime "invalid")]))

; perform e1[x |-> e2]
(subst : (LExp Symbol LExp -> LExp))
(define (subst e1 x e2)
  (type-case LExp e1
    [(varE s) (if (symbol=? s x) 
                  e2 
                  (varE s))]
    [(numE n) (numE n)]
    [(lamE id body) 
    (if (symbol=? x id) 
      (lamE id body)              ; shadowing case
      (lamE id (subst body x e2)))]
    [(appE e1App e2App) 
     (appE (subst e1App x e2) 
           (subst e2App x e2))]))

(define (interp e)
  (type-case LExp e
    [(varE s) (error 'runtime "unbound symbol")]
    [(lamE id body) (funV id body)]
    [(numE n) (numV n)]
    [(appE e1 e2)
     ; run e1 to get (lambda (id) body)
     ; run e2 to get a value argV
     ; run body[id |-> v]
     (letrec [(e1V (interp e1))
              (body (get-body e1V))
              (id (get-arg e1V))
              (argV (interp e2))]
       (type-case Value argV
         [(funV argId argBody)
          (interp (subst body id (lamE argId argBody)))]
         [(numV n)
          (interp (subst body id (numE n)))]))]))
  • Let’s try it on some examples:
> (interp (appE (lamE 'x (varE 'x)) (numE 10)))
- Value
(numV 10)

; example 1 above
(interp (appE (lamE 'x (varE 'x)) (lamE 'y (varE 'y))))
- Value
(funV 'y (varE 'y))

; example 2 above
; ((lambda (y) (lambda (x) y)) 10)
> (interp (appE (lamE 'y (lamE 'x (varE 'y))) (numE 10)))
- Value
(funV 'x (numE 10))

; example 3 above
; (lambda (x) (lambda (x) x) 10)
> (interp (appE (lamE 'x (lamE 'x (varE 'x))) (numE 10)))
- Value
(funV 'x (varE 'x))

Omega

  • It might not seem like it, but our little tiny language actually supports infinite computation! How is that possible?
  • Now let’s see a fun example: a term that runs forever!
  • This term is called the omega term
  • First, let’s run it by hand:
((λ x. (x x)) (λ x. (x x)))
--> (x x)[x |-> (λ x. (x x))]
--> ((λ x. x x) (λ x. x x))
--> (x x)[x |-> (λ x. (x x))]
--> ...
  • If you run this in our interpreter, it will run forever!
> (interp (appE
           (lamE 'x (appE (varE 'x) (varE 'x)))
           (lamE 'x (appE (varE 'x) (varE 'x)))))
  • Can you come up with some other terms that run forever?

Programming in the λ calculus

  • In what sense is the λ-calculus a “universal model” for computation?
  • We should be able to take any program written in any programming language and come up with a lambda-calculus term that is somehow equivalent. This seems like a tall order!
  • Let’s see some examples to get a feel for this. Consider the following Python program:
def andarg(x, y):
  return x and y
andarg(True, False)
  • This has several features the λ-calculus doesn’t have! Multi-argument functions, Booleans, etc.
  • How can we go about writing down an equivalent program to this in the λ calculus?
  • Let’s tackle these features one at a time. Multi-argument functions seem relatively easy, the λ-calculus can handle those by nesting λ abstractions. If we imagine that we somehow have a λ calculus implementation of and, we could write this imaginary program:
(((λ x. (λ y. x and y)) true) false)
  • This transformation of multi-argument functions into a function that returns another function is called currying after Haskell Curry.

Church encoding

  • Multi-argument functions weren’t too bad, so what about Booleans?
  • The key idea is to pick a lambda term that represents each Boolean value. We call these values Church encodings of Booleans.
    • Encoding of true: true = (λ x. (λ y. x)) “return the first argument”
    • Encoding of false: false = (λ x. (λ y. y)) “return the second argument”
  • Why these particular encodings? Well, we are not forced to pick these particular values (in fact, a fun exercise is to come up with some alternative Church encoding schemes for Booleans), but they will permit us to define related operations on Booleans
  • So, how do we define and? Intuitively, and is a function that should take “two arguments” (so, it is curried) and behaves roughly as follows:
    • ((and true) true) = true
    • ((and false) true) = false
  • Now, how can we manipulate these encoded Boolean values? First, we can define a function ite that acts like if:
ite = (λ g. (λ t. (λ e. ((g t) e))))
  • What is this doing? Intuitively, imagine g is true: then, it picks its first argument (t); otherwise, it picks its second argument (e)
  • We can run this to see it happen:
(ite true) = ((λ g. (λ t. (λ e. ((g t) e)))) true)
           --> (λ t. (λ e. ((g t) e)))[g |-> true]
           --> (λ t. (λ e. ((true t) e)))
  • The above function will return the value t, which is intuitively the then-branch of the if.
  • Let’s see it happen:
((ite true) true) = ((λ t. (λ e. ((true t) e))) true)
                  --> (λ e. ((true t) e))[t |-> true]
                  --> (λ e. ((true true) e))
  • One more step:
(((ite true) true) false) = ((λ e. ((true true) e)) false)
                          --> ((true true) e)[e |-> false]
                          --> ((true true) false)
                          --> true
  • and is very similar to ite: and = λ a. λ b. ((a b) false); intuitively, and is defined as “if a then b else false

An environment-passing implementation of lam

  • Similar to let, we can give a new stepping rule for lambda terms involving environments:
[], ((lambda (x) x) 5)
--> [x |-> 5], x
--> 5
  • A trickier case:
(let ([x 10])
    (let [(addxy (lambda (y) (+ x y)))]
      (addxy 5)))
  • How can we handle this? we need our functions to capture their context
(let ([x 10])
    (let [(addxy (lambda (y) (+ x y)))]
      (addxy 5)))
--> [x |-> 10], (let [(addxy (lambda (y) (+ x y)))]
      (addxy 5))
--> [x |-> 10, addxy |-> (lambda (y) (+ x y)), [x |-> 10]] (addxy 5)
--> [x |-> 10] (lambda (y) (+ x y)), []

Full implementation (worked through in class):

(define-type Exp
  [varE (s : Symbol)]
  [numE (n : Number)]
  [lamE (var : Symbol) (body : Exp)]
  [appE (fun : Exp) (arg : Exp)])

(define-type Value
  [numV (n : Number)]
  [funV (arg : Symbol) (body : Exp) (closure : Env)])

(define-type-alias Env (Hashof Symbol Value))
(define mt-env (hash empty)) ;; "empty environment"

(define (lookup (s : Symbol) (n : Env))
  (type-case (Optionof Value) (hash-ref n s)
    [(none) (error 'runtime "not bound")]
    [(some v) v]))

(extend : (Env Symbol Value -> Env))
(define (extend old-env new-name value)
  (hash-set old-env new-name value))


(interp : (Exp Env -> Value))
(define (interp e env)
  (type-case Exp e
    [(varE s) (lookup s env)]
    [(numE n) (numV n)]
    [(lamE v b) (funV v b env)]
    [(appE fun arg)
     (let [(evalfun (interp fun env))
           (evalarg (interp arg env))]
       (type-case Value evalfun
         [(funV arg body closure)
          ; run body with environment closure[arg |-> evalarg]
          (interp body (extend closure arg evalarg))]
         [(numV n) (error 'runtime "trying to run a number")]))]))

(test (interp (appE (lamE 'x (varE 'x)) (numE 10)) mt-env) (numV 10))
(test (interp (appE (lamE 'x (varE 'x)) (lamE 'y (varE 'y))) mt-env) (funV 'y (varE 'y) mt-env))
(test (interp (appE (lamE 'x (lamE 'x (varE 'x))) (numE 10)) mt-env) (funV 'x (varE 'x) (hash (list (pair 'x (numV 10))))))