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 ofhashset
with an entry that mapsnewkey
tonewvalue
hash-ref hashset key
, which returns anOptionof Value
, which isnone
ifkey
is not in the hashtable andsome v
ifv
is 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 bodye
which 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 namex
for the expressione2
ase1[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
x
in the body of the lambda? Intuitively, it refers to thex
that 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
x
inside 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
x
in the body of the lambda? Intuitively, it refers to thex
that 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
x
inside 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
x
in the lambda shadows the outerx
. This is intuitive, we expectx
to 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
let
To begin, let’s consider a core language called
lam
that 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
let
in 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