Lecture 6: Lambda calculus I
Goals for today:
- Implementing substitution efficiently with environments for
let - Develop
lam, a language with first class values- Understand substitution in
lam
- Understand substitution in
Logistics
- Steven’s office hours shifted this week by 30 minutes (11:30 – 12:30)
- New homework released soon
- Remember: quiz next Thurs/Fri, see previous week’s notes
Implementing let with environments
- Q: What is the runtime of our substitution algorithm?
- A: Linear in the size of the program!
- This runtime seems quite undesirable; every time we introduce a new variable, we will have to scan over the whole program text!
- No real program implementations use substitution this way: most implementations make use of an environment for keeping track of which variables are in-scope
- An environment is a map from identifiers to values. We write these as [x |-> v1, y |-> v2, …]
- We can run an interpreter by hand that manipulates environments. Now our arrow rules produce a sequence of
(environment, program)pairs:
[], (let (x 10) (let (y 20) (+ x y)))
--> [x |-> 10], (let (y 20) (+ x y))
--> [x |-> 10, y |-> 20], (+ x y)
--> [x |-> 10, y |-> 20], (+ 10 y)
--> [x |-> 10, y |-> 20], (+ 10 20)
--> [x |-> 10, y |-> 20], 30
Implementing an interpreter
- To model the environment, we use a hashset datatype:
(define-type-alias Env (Hashof Symbol Value))
(define mt-env (hash empty)) ;; "empty environment"
- There are two key functions for datatypes of type
Hashof Keytype Valuetype:extend hashset newkey newvalue, which creates a new hashset out ofhashsetwith an entry that mapsnewkeytonewvaluehash-ref hashset key, which returns anOptionof Value, which isnoneifkeyis not in the hashtable andsome vifvis in the hashtable
- We will implement some helper functions to deal with environments:
(define (lookup (s : Symbol) (n : Env))
(type-case (Optionof Number) (hash-ref n s)
[(none) (error s "not bound")]
[(some v) v]))
(extend : (Env Symbol Number -> Env))
(define (extend old-env new-name value)
(hash-set old-env new-name value))
- Now, we are ready to implement our evaluator. What should it do?
(define (interp e nv)
(type-case Exp e
[(numE n) n]
[(varE s) (lookup s nv)]
[(plusE l r) (+ (interp l nv) (interp r nv))]
[(let1E var val body)
(let ([new-env (extend nv
var
(interp val nv))])
(interp body new-env))]))
- And we can run this on some examples:
(test (interp (let1E 'x (numE 10)
(let1E 'x (numE 20)
(varE 'x))) mt-env) 20)
(test (interp (let1E 'x (numE 10) (plusE (varE 'x) (varE 'x))) mt-env) 20)
(test (interp (let1E 'x (numE 10)
(plusE (varE 'x)
(let1E 'x (numE 20) (varE 'x)))) mt-env) 30)
(test (interp (plusE (let1E 'x (numE 10) (varE 'x)) (let1E 'x (numE 15) (varE 'x))) mt-env) 25)
- Ponder: How does our interpreter deal with variables going out of scope? Why did that last example work?
First-class functions: the lambda calculus
- Now we begin our journey into a rich and beautiful language: the lambda-calculus
- In the lambda calculus, functions are values (in other words, we say functions are first-class in our language).
- Plait has first-class functions:
> (lambda (x) (+ x 1 ))
- (Number -> Number)
#<procedure>
- Syntactically, a lambda
(lambda (x) e)consists of two parts: an argumentx, which must be a symbol, and a bodyewhich can be any Plait expression - Lambdas are sometimes called anonymous functions because they do not have to be given a name when they are declared.
- We can call lambdas just like we would any other function:
> ((lambda (x) (+ x 1)) 10)
- Number
11
- Calling a lambda involves substitution. Let’s add some notation for substitution: for an expression
e1, we denote substituting the namexfor the expressione2ase1[x |-> e2] - Then, we can run this program by hand:
((lambda (x) (+ x 1)) 10)
--> (+ x 1)[x |-> 10]
--> (+ 10 1)
--> 11
- Just because lambdas can be anonymous doesn’t mean they have to be: we can bind lambda to names just like any other value by using
let. This lets us conveniently create local functions:
> (let [(addone (lambda (x) (+ 1 x)))]
(addone 10))
- Number
11
- Let’s run this by hand:
(let [(addone (lambda (x) (+ 1 x)))]
(addone 10))
--> (addone 10)[addone -> (lambda (x) (+ 1 x))]
--> ((lambda (x) (+ 1 x)) 10)
--> (+1 x)[x |-> 10]
--> (+ 1 10)
--> 11
- Lambdas have delicate scoping rules which we should investigate carefully
- Now for another interesting case: What should this program do?
> (let ([x 10])
(let [(addxy (lambda (y) (+ x y)))]
(addxy 5)))
- What is the scope of
xin the body of the lambda? Intuitively, it refers to thexthat is in-scope when the lambda is defined: lambdas hold onto values that are in-scope when they are created. - Let’s run this by hand:
(let ([x 10])
(let [(addxy (lambda (y) (+ x y)))]
(addxy 5)))
--> (let [(addxy (lambda (y) (+ x y)))]
(addxy 5))[x |-> 10]
--> (let [(addxy (lambda (y) (+ 10 y)))]
(addxy 5))
...
--> 15
- This seems simple enough, but can lead to some surprising subtleties! Look at this program:
> (let [(x 10)]
(let [(addx (lambda (y) (+ x y)))]
(let [(x 20)]
(addx 5))))
This program should output 15! Why? Remember dynamic scope: we want the scope of a name to be independent of the runtime behavior of the program. So, the scope of
xinside the body of the lambda should not depend on subsequently defined local variables.Now for another interesting case: What should this program do?
> (let ([x 10])
(let [(addxy (lambda (y) (+ x y)))]
(addxy 5)))
- What is the scope of
xin the body of the lambda? Intuitively, it refers to thexthat is in-scope when the lambda is defined: lambdas hold onto values that are in-scope when they are created. - Let’s run this by hand:
(let ([x 10])
(let [(addxy (lambda (y) (+ x y)))]
(addxy 5)))
--> (let [(addxy (lambda (y) (+ x y)))]
(addxy 5))[x |-> 10]
--> (let [(addxy (lambda (y) (+ 10 y)))]
(addxy 5))
...
--> 15
- This seems simple enough, but can lead to some surprising subtleties! Look at this program:
> (let [(x 10)]
(let [(addx (lambda (y) (+ x y)))]
(let [(x 20)]
(addx 5))))
- This program should output 15! Why? Remember dynamic scope: we want the scope of a name to be independent of the runtime behavior of the program. So, the scope of
xinside the body of the lambda should not depend on subsequently defined local variables.
Basic semantics of lam
- What do we expect the following program to do?
(let [(x 10)]
(let [(addone (lambda (x) (+ 1 x)))]
(addone x)))
- We expect it to return 11. Why? We see that the
xin the lambda shadows the outerx. This is intuitive, we expectxto refer to its definition in the inner-most scope. - let’s run this by hand:
(let [(x 10)]
(let [(addone (lambda (x) (+ 1 x)))]
(addone x)))
--> (let [(addone (lambda (x) (+ 1 x)))]
(addone x))[x |-> 10]
--> (let [(addone (lambda (x) (+ 1 x)))]
(addone 10))
--> (addone 10)[addone |-> (lambda (x) (+ 1 x))]
--> (addone 10)[addone |-> (lambda (x) (+ 1 x))]
--> ((lambda (x) (+ 1 x)) 10)
--> (+ 1 x)[x |-> 10]
--> (+ 1 10)
--> 11
So, lambdas have scoping rules that are quite similar to
letTo begin, let’s consider a core language called
lamthat consists of three syntactic constructs:
(define-type LExp
[varE (n : Symbol)] ; variable reference
[lamE (x : Symbol) (body : LExp)] ; lambda introduction
[appE (e : LExp) (arg : LExp)]) ; lambda application: run function e1 with argument
- Now let’s consider the semantics. What is the right notion of a value here?
- We want our language to support “first-class functions”, meaning functions can be data, so let’s make our value type support lambdas:
(define-type Value
[funV (arg : Symbol) (body : Lam)])
- Then, we can define our evaluator in terms of substitution (but note, this evaluator is not in its final form and has some subtle issues):
(define-type LExp
[varE (s : Symbol)]
[lamE (x : Symbol) (body : LExp)]
[appE (e : LExp) (arg : LExp)])
(define-type Value
[funV (arg : Symbol) (body : LExp)])
(define (get-arg v)
(type-case Value v
[(funV arg body) arg]))
(define (get-body v)
(type-case Value v
[(funV arg body) body]))
; 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))]
[(lamE id body) (if (symbol=? x id) (lamE id body) (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)]
[(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))]
(interp (subst body id (lamE (get-arg argV) (get-body argV)))))]))
- We can try this on a few inputs:
(interp (appE (lamE 'y (varE 'y)) (lamE 'x (varE 'x))))
- Value
(funV 'x (varE 'x))
- Here is a very interesting example called Omega, which runs forever!
(define om (lamE 'x (appE (varE 'x) (varE 'x))))
(interp (appE om om))
- Note: we can write
letin terms oflambda!
(let [(x 10)]
(+ 1 x))
=
((lambda (x) (+ 1 x)) 10)
- In general, we can translate
(let1 ('x e1) e2)into a lambda term(lambda (x) e2) e1