eval for expressions
based on a notion of reduction. We say that
an expression
e evaluates to an answer a
if and only if e reduces to a:
eval(e) = a if and only if e ->* a.
The reduction relation ->* is the reflexive
and transitive closure of the simpler reduction relation
->. That is, ->* consists of
zero or more -> steps.
To define ->, we need some preliminary definitions.
Expressions e and values v
are:
e ::= v | x | (e e)
v ::= c | (lambda (x) e)
Evaluation contexts E are expressions with a hole,
denoted []. We can define evaluation contexts inductively:
E ::= [] | (E e) | (v E) | (if E e e)
For Scheme where applications may have more than one argument,
we have
E ::= [] | (v* E e*) | (if E e e).
A redex is one of the following
((lambda (x) e) v)
(if v e2 e3)
(add1 n)
... other primitives
Now we can define the reduction relation ->:
E[((lambda(x) e) v)] -> E[e[x |-> v]] (substitution avoiding capture)
E[(if v e2 e3)] -> E[e3] if v = #f
E[(if v e2 e3)] -> E[e2] if v != #t
E[(add1 n)] -> E[n+1]
...
Lemma: For all closed e, either e = v or there is a unique E and redex such that e = E[redex].
Lemma: For e closed, if e -> e' then e' is closed.
From these two lemmas, we get the following rather desirable property:
Thm: The relation -> is a function.
This is good because it means that a program will always produce the same answer.
We have yet to formally define capture avoiding substitution |->.
Let's do that:
c[x |-> v] = c
x[x |-> v] = x
y[x |-> v] = y (y != x)
(e1 e2)[x |-> v] = (e1[x |-> v] e2[x |-> v])
(lambda (x) e)[x |-> v] = (lambda (x) e)
(lambda (y) e)[x |-> v] = (lambda (z) e[y |-> z][x |-> v]) (z not in FV(e) or FV(v))
In the last clause, renaming y to z is how we
avoid capturing free occurrences of y in v
when substituting inside the (lambda (y) ...).
Now let's do an example:
((lambda (x) (x x)) (lambda (y) y y))
-> (x x)[x |-> (lambda (y) (y y))] = ((lambda (y) (y y)) (lambda (y) (y y)))
-> (y y)[y |-> (lambda (y) (y y))] = ((lambda (y) (y y)) (lambda (y) (y y)))
-> ...
At each step above, the evaluation context E is just [].
By the way,
since names don't matter it is clear that we are right back where we started.
This is an example of an infinite loop without using letrec.
Here's another example:
(sub1 ((lambda (x) (add1 x)) 1)) E = (sub1 [])
-> (sub1 (add1 x)[x |-> 1]) = (sub1 (add1 1)) E = (sub1 [])
-> (sub1 2) E = []
-> 1
Scheme programs consist of more than simply single expressions.
define allows us to make definitions at the top level scope. With
this in mind we can augment our reduction rules to handle this situation.
P ::= D* e
D ::= (define x v*)
D* E[((lambda (x) e) v)] -> D* E[e[x |-> v]
D* E[(add1 n)] -> D* E[n+1]
...
D1*(define x v)D2* E[x] -> D1*(define x v)D2* E[v]
This last rule allows "looking up" the name of a definition.
We now have reduction semantics sufficient to evaluate the following expression:
(define Pi
(lambda (l)
(if (null? l)
1
(* (car l) (Pi (cdr l))))))
(Pi '(1 2))
E = ([] '(1 2)) :> D((lambda (l) ... ) '(1 2))
E = [] :> D(if (null? '(1 2)) 1 (* (car '(1 2)) (Pi (cdr '(1 2)))))
E = (if [] 1 ... ) :> D(if #f 1 (* ...))
...
:> D(* 1 (* 2 1))
We can also do the exact same thing for the CPS version of Pi.
(define CPS-Pi
(lambda (l k)
(if (null? l) (k 1)
(CPS-Pi (cdr l) (lambda (v) (* (car l) v))))))
(CPS-Pi '(1 2) (lambda (x) x))
E = [] :> D((lambda (l) ...) '(1 2) (lambda (x) x))
E = [] :> (if (null? l) 1 (CPS-Pi ...))
E = [] :> (if #f 1 (CPS-Pi ...))
E = [] :> (CPS-Pi (cdr '(1 2)) (lambda (v) ...))
Note that E = [] throughout this reduction. This means
that no context is built. A call that builds no context is called a
tail call. A procedure whose recursive calls are all tail calls
is tail recursive. Tail recursive procedures in Scheme are
iterative, like loops, because Scheme implementations are required to build
no context for tail calls. In other words, tail calls never return. In
contrast, C does not usually compile tail calls in this way. Try writing
addition as a tail recursive procedure using successor, and you will likely get a stack overflow.
The following is an algorithm for performing CPS.
CPS: expr X expr -> expr
CPS(x, k) = (k x)
CPS(c, k) = (k c)
CPS((lambda (x) e), k) = (k (lambda (x k) CPS(e, k)))
CPS((e1 e2), k) = CPS(e1, (lamdba (x) CPS(e2, (lambda (y) (x y k)))))
CPS((if e1 e2 e3), k) = CPS(e1, (lambda (x) (if x CPS(e2,k) CPS(e3,k))))
Many (but not all) compilers for advanced languages like Scheme and ML
use this algorithm to transform all user programs into CPS as one step
of compiling. Then all calls in a program are tail calls. Hence no
call ever returns and procedure calls thus become jumps with arguments.