CSU660 Scheme Evaluation RulesFormal rules for evaluating Scheme Using substitution. | |
Preliminaries
These are rules for evaluating Scheme using the substitution model. There
are separate rules for every kind of Scheme expression we want to handle.
There are certain notational conventions to be aware of:
- Fixed-width font is used for Scheme code.
- Scheme code often contains meta names which stand for some other
Scheme code, a different font is used for them.
- For the meta names, we use different letters for things that
stand for different types of Scheme code:
- E1, E2, ..., En: expressions
- x1, x2, ..., xn: identifiers
- v1, v2, ..., vn: values
Note that in DrScheme you can switch to one of the “Student” languages
and use the stepper to see these rules in action.
The rules follow, divided into groups.
Basic evaluation
These rules cover basic Scheme forms that are not special forms.
- Evalprim
- Primitive value expressions have their obvious values. To evaluate
such an expression, you simply take its value. These include:
- Numbers and strings.
- Primitive operators and functions such as +, - and
sin having function values that are given as built-ins
(primitive functions aren't actually symbols, but we use the same
symbol is used for the name and for its value).
- Lambda expressions are also values.
(Usually primitive values are obvious, so this rule is used
implicitly.)
- Evalvar
- Identifiers denote bindings and are implemented as symbols. To
evaluate an expression x which is an identifier, you return this
identifier's value which should be a value that you know by the
Evaldef rule below. Note that (a) this does not involve
further evaluation, and (b) it applies only for global
identifiers since local ones are substituted away.
- Evalap
- To evaluate a compound form (E1 E2 ... En), which is not a special
form (see below), you evaluate each of its sub-expressions
recursively, getting a list of values (v1 v2 ... vn). Then
continue with one of the Apply• rules to apply the function v1 on
the arguments v2, ...vn. This rule is the default that
is used only when the expression is not one of the special form
cases,so its place should be at the end of the Eval• rules, but it
is here for clarity.
Application rules
These are the two application rules. Note that these rules deal with
evaluated inputs.
- Applyprim
- To apply a primitive function f on arguments v1, v2, ..., vn,
you simply compute the function result. This uses knowledge about
the function which is taken as built-in — it is not part of these
rules.
Example:
(+ 3 4)
Applyprim⇒ 7
(car (cons v1 v2))
Applyprim+Applyprim⇒ v1 ; after two usages |
- Applylam
- To apply a lambda expression (lambda (x1...xn) E) on some
given value arguments v1, v2, ..., vn, you substitute the
values for the argument names in the body of the lambda
expression: E[v1, v2, ..., vn/x1, x2, ..., xn].
In other words, you substitute every free occurrence of xi in E by
the corresponding vi value.
You then (recursively) continue the evaluation process with the
result.
Examples:
((lambda (x y) (+ x y)) 3 4)
Applylam⇒ (+ 3 4)
Applyprim⇒ 7
((lambda (x) (lambda (y) (+ x y))) 3)
Applylam⇒ (lambda (y) (+ 3 y)) |
Special forms
The rest are special-form rules. Here we have only two basic rules, and
then later a few rules for derived expressions.
- Evaldef
- To evaluate a simple define expression (define x E) where
x is an identifier, you evaluate E recursively to get a value
v, and you mark to yourself that now you now know that the
value associated with the identifier x is v. By “know”
we mean some way that can be used in the Evalvar rules above.
Important: this rule can only be applied to top-level expressions.
- Evalif
- To evaluate an if form (if E1 E2 E3), you recursively
evaluate E1 to get a value v1. If v1 is true (anything other
than #f), then continue by evaluating (and returning the value
of) E2, and if v1 is #f, then continue with E3.
Derived forms
Finally, these forms are all derived forms, they are evaluated by
rewrite rules which transform them into a different form (eventually to a
form that uses only primitives).
- Evalfdef
- To evaluate a define expression that is a syntactic sugar for a
function definition, (define (x1 x2 ... xn) E), you continue
by evaluating the form (define x1 (lambda (x2 ... xn) E)).
- Evallet
- A let form is evaluated by this rewrite rule:
(let ([x1 E1] ... [xn En]) E)
⇒
((lambda (x1 ... xn) E) E1 ... En)
Essentially, what you get is to evaluate E with xi bound to the
result of evaluating Ei. For this reason, it is ok to skip the
rewritten form and use this rule together with the Applylam rule.
For example, instead of writing:
(let ([x 1] [y 2]) (+ x y))
Evallet⇒ ((lambda (x y) (+ x y)) 1 2)
Applylam⇒ (+ 1 2) |
you can write:
(let ([x 1] [y 2]) (+ x y))
Evallet+Applylam⇒ (+ 1 2) |
But keep in mind that in both of these cases you should evaluate the
expressions 1 and 2 before you do the substitutions. Also,
note that the translation step makes things more verbose but using it
makes it harder to make a mistakes, for example, it is clear that if
you replace the 2 in the original expression by x, then this
x is a free (unbound) identifier.
- Evallet*
- This rewrite rule has two cases for a let* form with either zero
or more bindings:
- (let* ([x1 E1] [x2 E2] ... [xn En]) E)
⇒
(let ([x1 E1]) (let* ([x2 E2] ... [xn En]) E))
- (let* () E) ⇒ (let () E)
Again, you can use your intuition here, and bind the names, keeping
in mind that you do it one by one — and you can combine this rewrite
with the Evallet and even combine that further with the Applylam
rule. For example, instead of:
(let* ([x 1] [y x]) (+ x y))
Evallet*⇒ (let ([x 1]) (let* ([y x]) (+ x y)))
Evallet⇒ ((lambda (x) (let* ([y x]) (+ x y))) 1)
Applylam⇒ (let* ([y 1]) (+ 1 y))
Evallet*⇒ (let ([y 1]) (let* () (+ 1 y)))
Evallet⇒ ((lambda (y) (let* () (+ 1 y))) 1)
Applylam⇒ (let* () (+ 1 1))
Evallet*⇒ (let () (+ 1 1))
Evallet⇒ ((lambda () (+ 1 1)))
Applylam⇒ (+ 1 1)
Applyprim⇒ 2 |
you can write:
(let* ([x 1] [y x]) (+ x y))
Evallet*+Evallet+Applylam⇒ (let* ([y 1]) (+ 1 y))
Evallet*+Evallet+Applylam⇒ (let* () (+ 1 1))
Evallet*+Evallet+Applylam⇒ (+ 1 1)
Applyprim⇒ 2 |
The same notes as above hold here.
- Evalcond
- Again, a rewrite rule with several cases:
- (cond [E1,1 E1,2] [E2,1 E2,2] ... [En,1 En,2])
⇒
(if E1,1 E1,2 (cond [E2,1 E2,2] ... [En,1 En,2]))
- (cond [E1,1 E1,2])
⇒
(if E1,1 E1,2 #f)
- (cond [else E]) ⇒ E
Note that this makes cond return false when no conditions match
and no else clause is given — this is different from “real”
Scheme, but we use this due to the lack of undefined values and
errors (our if form must have two sides).
- Evaland
- (and) ⇒ #t
- (and E) ⇒ E
- (and E1 E2 ... En)
⇒ (if E1 (and E2 ... En) #f)
- Evalor
- (or) ⇒ #f
- (or E) ⇒ E
- (or E1 E2 ... En)
⇒ (let ([x1 E1]) (if x1 x1 (or E2 ... En)))
Note the need for a let in the last case.