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]
substitutesx
fore2
ine1
- 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”
- Encoding of true:
- 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 likeif
:
ite = (λ g. (λ t. (λ e. ((g t) e))))
- What is this doing? Intuitively, imagine
g
istrue
: 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 toite
:and = λ a. λ b. ((a b) false)
; intuitively,and
is defined as “ifa
thenb
elsefalse
”
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))))))