M ::= x
| MM
| λx.M
| N
N ::= 1 | 2 | 3 | ...
- We don't need
Nfor lambda calculus, but it makes it easier to understand - Left associative, so
M₁M₂M₃is read as(M₁M₂)M₃ - But
λx.xyisλx.(xy)
- Variables that have no value
- If a lambda expression has no free variables, it is called closed
z(x y):z,x,yare freeλx.x: none are freeλx.y x:yis freeλy.λx.y x: none are free(λy.λx.y x) x:xis free(λx.x)(λy.x) y:yand secondxare free
M₁[x |-> M₂]- Set
xtoM₂in the context ofM₁
- Set
- For example,
(y x)[x |-> 5]becomes(y 5)(x x)[x |-> y]becomes(y y)(x x)[y |-> x]becomes(x x)(z y)[y |-> λa.a]becomes(z (λa.a))
Beta reduction is the application of a value to a variable:
((λx.M₁)M₂) ->β (M₁[x |-> M₂])
For example:
(λf.f 5)(λx.x)
->β (λx.x) 5
->β 5
(λz.(λp.p(p z))(λy.y))2
->β (λp.p(p 2))(λy.y)
->β (λy.y)((λy.y) 2)
->β (λy.y)2
->β 2
Beta reduction has issues:
- Not deterministic. You can have two possible beta reductions in one go
- Not efficient. Copying the entirety of a value into all references will not be quick This makes it not a practical language.
let x = M₁ in M₂ becomes (λx.M₂)M₁
((λx.xx)(λx.xx)) can loop forever - TODO: Is this useful?
(M₁, M₂) = λv.v M₁ M₂fst = λp.p(λx.λy.x)snd = λp.p(λx.λy.y)
fst (M₁, M₂)
-> (λp.p(λx.λy.x)) (λv.v M₁ M₂)
->β (λv.v M₁ M₂)(λx.λy.x)
->β (λx.λy.x) M₁ M₂
->β (λy.M₁) M₂
->β M₁
Cstands for code (or control)- The expression the machine is currently trying to evaluate
Estands for environment- Holds the bindings of free variables in
C
- Holds the bindings of free variables in
Kstands for continuation- Holds what the machine to do once finished with
C
- Holds what the machine to do once finished with
A value W is defined as:
W ::= n
| clos(λx.M, E)
Where n is a constant, and clos(...) is a closure including x, M, and
E.
The environment is a list of values:
E = {x₁ |-> W₁, x₂ |-> W₂, ...}
E = {} = ∅
Adding a binding is written as E[x |-> W]
Finding a binding is written as lookup x in E
A continuation is a stack of frames. The empty stack is ■.
A frame is:
F ::= (W ○)
| (○ M E)
Where:
Wis some evaluated valueMis something to evaluateEis the environment
To evaluate M₁M₂, we need to:
- Evaluate
M₁gettingW₁, pushing the frame(○ M₂ E)so we can later evaluate M₂ - Evaluate
M₂(popped from stack) gettingW₂, and push the frame(W₁ ○)to the stack.W₁is pushed on to the stack. - Apply
W₁toW₂.
<x | E | K> → <lookup x in E | E | K>
If we have a variable x, we must look it up in E and set the value to be
the current code
<M₁M₂ | E | K> → <M₁ | E | (○ M₂ E), K>
If we have an application of M₁M₂, we
- Set the current code to
M₁ - Push onto the continuation
M₂along with the current environmentE
<λx.M | E | K> → <clos(λx.M, E) | E | K>
If we have a lambda expression, close it with the current environment
<W | E₁ | (○ M E₂), K> → <M | E₂ | (W ○), K>
If C is a constant or closure W, and we've got a RHS to process on the
stack, swap the two. This discards the current environment
<W | E₁ | (clos(λx.M, E₂) ○), K> → <M | E₂[x |-> W] | K>
If C is a constant or closure W, and we've got a closure to process on the
stack, bind W to the closure value
We say M evaluates to W if:
<M | ø | ■> →* <W | E | ■>
Examples:
<((λx.λy.x) 1) 2 | ∅ | ■>
-> <(λx.λy.x) 1 | ∅ | (○ 2 ∅)>
-> <λx.λy.x | ∅ | (○ 1 ∅), (○ 2 ∅)>
-> <clos(λx.λy.x, E) | ∅ | (○ 1 ∅), (○ 2 ∅)>
-> <1 | ∅ | (clos(λx.λy.x, E) ○), (○ 2 ∅)>
-> <λy.x | {x |-> 1} | (○ 2 ∅)>
-> <clos(λy.x, {x |-> 1}) | {x |-> 1} | (○ 2 ∅)>
-> <2 | ∅ | (clos(λy.x, {x |-> 1}) ○)>
-> <x | {x |-> 1, y |-> 2} | ■>
-> <2 | {x |-> 1, y |-> 2} | ■>
<(λf.f 2)(λx.x) | ∅ | ■>
-> <λf.f 2 | ∅ | (○ (λx.x) ∅)>
-> <clos(λf.f 2, ∅) | ∅ | (○ (λx.x) ∅)>
-> <λx.x | ∅ | (clos(λf.f 2, ∅) ○)>
-> <f 2 | {f |-> λx.x} | ■>
-> <f | {f |-> λx.x} | (○ 2 {f |-> λx.x})>
-> <λx.x | {f |-> λx.x} | (○ 2 {f |-> λx.x})>
-> <clos(λx.x, {f |-> λx.x}) | {f |-> λx.x} | (○ 2 {f |-> λx.x})>
-> <2 | {f |-> λx.x} | (clos(λx.x, {f |-> λx.x}) ○)>
-> <x | {f |-> λx.x, x |-> 2} | ■>
-> <2 | {f |-> λx.x, x |-> 2} | ■>
- Constant propagation
- Easier than in imperative languages, as symbols stay constant
- Function inlining still possible
let f = λx.M in ...f V...- Becomes
let f = λx.M in ...M[x |-> V]...
The language is now extended:
M ::= ...
| go N
| here M
With an extended stack:
F ::= ... | »
<here M | E | K> → <M | E | », K>
If we encounter a here M, push a » onto the stack
<go N | E | K₁, », K₂> → <N | E | K₂>
If we encounter a go N, go to after the next » in the stack. K₁ and K₂
are sequences in the stack, and K₁ does not contain any »
<W | E | », K> → <W | E | K>
If we encounter a » on the stack, ignore it
<here ((λx.2)(go 5)) | ∅ | ■>
-> <(λx.2)(go 5) | ∅ | »>
-> <λx.2 | ∅ | (○ (go 5) ∅), »>
-> <clos(λx.2, ∅) | ∅ | (○ (go 5) ∅), »>
-> <go 5 | ∅ | (clos(λx.2, ∅) ○), »>
-> <5 | ∅ | ■>
- Every variable is only assigned once
- Equivalent of
letbut in intermediate languages (LLVM-IR)
For example:
// Can't replace `x` with `5` because of modification
x = 5;
x = x + 1;
y = x * 2;
// Change to use `x₁` and `x₂`
x₁ = 5;
x₂ = x₁ + 1;
y = x₂ + 1;
// Allows us to replace `x₁` with `5`
x₂ = 5 + 1;
y = x₂ + 1;i.e. Two programs behave the same
M can be reduced to n - written as M↓n:
<M | ø | ■> ->* <n | E | ■>
A context C is a "term with a hole":
C ::= ○
| M C
| C M
| λx.C
We write C[M] for the term we get by plugging M into the hole position ○
in C.
M₁ and M₂ are contextually equivalent if for all contexts C and integers
n:
C[M₁]↓n iff C[M₂]↓n
This means we can replace M₁ with M₂ if it is faster (and vice versa).