When we first used Advanced Student Scheme, we did so because we wanted to deal with functions as ordinary values. The evaluation rules barely changed. We just agreed to allow expressions in the first position of an application and to deal with the names of functions as values.
The extension of the language with set!-expressions required another change to our rules. Now definitions that associate variables and values can change over the course of an evaluation. The informal rules we've used so far deal with changes to the definition of state variables, because they matter the most. But the rules are informal and imprecise, so a precise description of how the addition of set! changes the meaning of the language must be our primary concern.
Let's recall how we determine the meaning of a program. A program consists
of two parts: a collection of definitions and an expression. The goal is to
evaluate the expression, which means to determine the expression's
value.
In
Beginning Student Scheme, the collection of values consists of all
the constants plus lists. Only one list has a concise representation: the
empty one. All other lists are written down as a series of
constructed lists.
The evaluation of an expression consists of a series of steps. At each step we use the laws of arithmetic and algebra to simplify a subexpression. This yields another expression. We also say that we REWRITE the first expression into the second. If the second expression is a value, we are finished.
The introduction of set!-expressions into our programming language requires a few small adjustments and extensions to this process:
(+ (* 3 3) (* 4 4))we may choose to evaluate (* 3 3) and then (* 4 4) or vice versa. Fortunately, for such simple expressions, the choice doesn't affect the final outcome, so we don't have to supply a complete unambigous rule. In general, though, we rewrite subexpressions in a left-to-right and top-to-bottom order. At each stage in the evaluation, we best start by underlining the subexpression that must be evaluated next.
(define x aValue) ... ...The equation indicates that the program changes in two ways. First, the variable definition is modified. Second, the underlined set!-expression is replaced by (void), the invisible value.... = (define x anotherValue) ... ... (void) ...
Suppoe that the underlined expression is a (state) variable. Then we know that we can't make any progress in our evaluation until we have replaced the variable with the current value in its definition. This suggests the following revised law for variable evaluation:
(define x aValue) ... ...In short, substitute the value in a state variable definition for the state variable only when the value is needed for this particular occurrence of the state variable.... = (define x aValue) ... ... aValue ...
(begin v exp-1 ... exp-n) = (begin exp-1 ... exp-n)That means we also need a rule for dropping begin completely:
(begin exp) = expIn addition, we use a rule for dropping several values at once:
(begin v-1 ... v-m exp-1 ... exp-n) = (begin exp-1 ... exp-n)But this is only a convenience.
Let's consider some examples. The first one demonstrates how the order of evaluation of subexpressions makes a difference:
(define x 5) (+ (beginThe program consists of one definition and one addition, which is to be evaluated. One of the addition's arguments is a set!-expression that mutates x; the other is just x. By evaluating the subexpressions of the addition from left to right, the mutation takes place before we replace the second subexpression with its value. As a result, the outcome is 22. If we had evaluated the addition from right to left, the result would have been 16. To avoid such problems, we use the fixed ordering but give ourselves more freedom when no state variables are involved.x) x)
= (define x 11) (+ (begin (void)
) x)
= ...
= (define x 11) (+ 11
)
= (define x 11)
![]()
The second example illustrates how a set!-expression that occurs in a local-expression\ actually affects a top-level definition:
(define (make-counter x0)
(local ((define counter x0)
(define (increment)
(begin
(set! counter (+ counter 1))
counter)))
increment))
(
)
The program again consists of a single definition and an expression that is
to be evaluated. The latter, however, is an application nested in an
application. The inner application is underlined, because we must evaluate
it to make progress. Here are the first few steps:
= (define (make-counter x0)
(local ((define counter x0)
(define (increment)
(begin
(set! counter (+ counter 1))
counter)))
increment))
((local ((define counter 0)
(define (increment)
(begin
(set! counter (+ counter 1))
counter)))
increment))
= (define (make-counter x0)
(local ((define counter x0)
(define (increment)
(begin
(set! counter (+ counter 1))
counter)))
increment))
(define counter1 0)
(define (increment1)
(begin
(set! counter1 (+ counter1 1))
counter1))
(increment1)
The evaluation of the local-expression created additional top-level
expressions. One of them introduces a state variable; the others define
functions.
The second part of the evaluation determines what (increment1) accomplishes:
(define counter1 0)During the evaluation, we replace counter1 with its value twice. First, the second step replaces counter1 with 0, its value at that point. Second, we substitute 1 for counter1 during the last step, which is its new value.![]()
= (define counter1 0) (begin (set! counter1 (+
1)) counter1)
= (define counter1 0) (begin (set! counter1
) counter1)
= (define counter1 0) (begin
counter1)
= (define counter1 1) (begin (void)
)
= (define counter1 1) 1
Exercise 38.3.1
Underline the subexpression that must be evaluated next in the following expressions:
1. (define x 11)
(begin
(set! x (* x x))
x)
2. (define x 11)
(begin
(set! x
(cond
[(zero? 0) 22]
[else (/ 1 x)]))
'done)
3. (define (run x)
(run x))
(run 10)
4. (define (f x) (* pi x x))
(define a1 (f 10))
(begin
(set! a1 (- a1 (f 5)))
'done)
5. (define (f x)
(set! state (- 1 state)))
(define state 1)
(f (f (f)))
Explain why the expression must be evaluated. Solution
Exercise 38.3.2
Confirm that the underlined expressions must be evaluated next:
1. (define x 0)
(define y 1)
(begin
(set! y 4)
(+ (* x x) (* y y)))
2. (define x 0)
(set! x
(cond
[(zero?
) 1]
[else 0]))
3. (define (f x)
(cond
[(zero? x) 1]
[else 0]))
(begin
f)
Rewrite the three programs to show the next state. Solution
Exercise 38.3.3
Evaluate the following programs:
1. (define x 0)
(define (bump delta)
(begin
(set! x (+ x delta))
x))
(+ (bump 2) (bump 3))
2. (define x 10)
(set! x (cond
[(zeor? x) 13]
[else (/ 1 x)]))
3. (define (make-box x)
(local ((define contents x)
(define (new y)
(set! contents y))
(define (peek)
contents))
(list new peek)))
(define B (make-box 55))
(define C (make-box 'a))
(begin
((first B) 33)
((second C)))
Underline for each step the subexpression that must be evaluated next.
Show only those steps that involve a local-expression or a
set!-expression. SolutionIn principle, we could work with the rules we just discussed. They cover the common cases, and they explain the behavior of the programs we have encountered. They do not explain, however, how an assignment works when the left-hand side refers to a defined function. Consider the following example, for which the rules still work:
(define (f x) x)Here f is a state variable. The set!-expression changes the definition so f stands for a number. The next step in an evaluation substitutes 10 for the occurrence of f.(begin
f)
= (define f 10)
(begin (void) f)
Under ordinary circumstances, an assignment would replace a function definition with a different function definition. Take a look at this program:
(define (f x) x) (define g f) (+ (beginThe purpose of the underlined set!-expression is to modify the definition of f so that it becomes a function that always produces 22. But g stands for f initially. Since f is a the name of a function, we can think of (define g f) as a value definition. The problem is that our current rules change the definition of f and, by implication, the definition of g, because it stands for f:5) (g 1))
= (define f (lambda (x) 22)) (define g f) (+Scheme, however, does not behave this way. A set!-expression can modify only one definition at a time. Here it modifies two: f's, which is intended, and g's, which happens through the indirection from g to f. In short, our rules do not explain the behavior of all programs with set!-expressions; we need better rules if we wish to understand Scheme fully.(g 1))
= (define f (lambda (x) 22)) (define g f) (+ 5
)
= (define f (lambda (x) 22)) (define g f) (+ 5 22)
The problem concerns the definitions of functions, which suggests that we take a second look at the representation of functions and function definitions. So far, we used the names of functions as values. As we have just seen, this choice may cause trouble in case the state variable is a function. The solution is to use a concrete representation of functions. Fortunately, we already have one in Scheme: lambda-expressions. Furthermore, we rewrite function definitions so that they turn into variable definitions with a lambda-expression on the right-hand side:
(define (f x) x)Even recursive definitions are evaluated in this manner:= (define f (lambda (x) x))
(define (g x)
(cond
[(zero? x) 1]
[else (g (sub1 x))]))
= (define g
(lambda (x)
(cond
[(zero? x) 1]
[else (g (sub1 x))])))
All other rules, including the rule for replacing variables with their
values, remain the same.
Figure
specifies the set of values,
as a subset of the set of expressions, and the set of value
definitions, as a subset of the definitions. Using these definitions and
the modified rules, we can take a second look at at the above example:
The key difference is that the definition of g directly associates the variable with a function representation, not just a name for a function.(define g f) (+ (begin (set! f (lambda (x) 22)) 5) (g 1))
= (define f (lambda (x) x)) (define g
) (+ (begin (set! f (lambda (x) 22)) 5) (g 1))
= (define f (lambda (x) x)) (define g (lambda (x) x)) (+ (begin
5) (g 1))
= (define f (lambda (x) 22)) (define g (lambda (x) x)) (+
(g 1))
= (define f (lambda (x) 22)) (define g (lambda (x) x)) (+ 5
)
= (define f (lambda (x) 22)) (define g (lambda (x) x)) (+ 5 1)
The following program shows the effects of set!-expressions on functions with an extreme example:
(define (f x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))]))
(define g f)
(begin
(set! f (lambda (x) 'ouch))
(symbol=? (g 1) 'ouch))
The function f is recursive on natural numbers and always produces
'done. Initially, g is defined to be f. The
final begin-expression first modifies f and then uses g.
At first, we must rewrite the function definitions according to our modified rules:
= (define f
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(define g
)
(begin
(set! f (lambda (x) 'ouch))
(symbol=? (g 1) 'ouch))
= (define f
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(define g
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(begin
(set! f (lambda (x) 'ouch))
(symbol=? (g 1) 'ouch))
Rewriting the definition of f is straightforward. The major change
concerns the definition of g. Instead of f it now
contains a copy of the value for which f currently stands. This
value contains a reference to f, but that is not unusual.
Next, the set!-expression modifies the definition of f:
...
= (define f
(lambda (x)
'ouch))
(define g
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(begin
(void)
(symbol=?
'ouch))
No other definition, however, is affected. In particular, the definition of g remains the same, though the f inside of g's value now refers to a new value. But we have seen this phenomenon before. The next two steps follow the basic rules of intermezzo 1:
...
= (define f
(lambda (x)
'ouch))
(define g
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(begin
(void)
(symbol=? (f 0) 'ouch))
= (define f
(lambda (x)
'ouch))
(define g
(lambda (x)
(cond
[(zero? x) 'done]
[else (f (sub1 x))])))
(begin
(void)
(symbol=? 'ouch 'ouch))
That is, the application of g eventually applies f to 0, which yields 'ouch. Hence the final result is true.
Exercise 38.3.4
Validate that the following program evaluates to true:
(define (make-box x)
(local ((define contents x)
(define (new y) (set! contents y))
(define (peek) contents))
(list new peek)))
(define B (make-box 55))
(define C B)
(and
(begin
((first B) 33)
true)
(= (second C) 33)
(begin
(set! B (make-box 44))
(= (second C) 33)))
Underline for each step the subexpression that must be evaluated next.
Show only those steps that involve a local-expression or a
set!-expression. SolutionWhile we decided to rewrite function definitions so that their right-hand side are always lambda-expressions, we stuck with a function application rule that assumes function definitions in the style of Beginning Student Scheme. More concretely, if the definition context contains a definition such as
(define f (lambda (x y) (+ x y)))and the expression is
(* (f 1 2) 5)then the next step in the evaluation is
(* (+ 1 2) 5)For other occasions, however, we just replace variables with the values in the respective definitions. If we followed that rule, we would rewrite
(* (f 1 2) 5)to
(* ((lambda (x y) (+ x y))
1 2)
5)
At first glance, this exploration route ends here, because there are no
laws for this application.
We can reconcile the two ideas with a new law, suggested by the last expression:
((lambda (x-1 ... x-n) exp) v-1 ... v-n) = expThe law serves as a replacement of the law of application from algebra in the study of the foundations of computing. By convention, this law is called thex-1 ... x-n
v-1 ... v-n
Beta and the Lambda Calculus: The orginal
axiom was
formulated by Alonzo Church in the late 1920s as follows:
((lambda (x) exp) exp-1) = expIt does not restrict the argument in a function application to be a value. The interest of Church and other logiciansx
exp-1
are enough to define all computable functions on a (simulation of) the natural numbers. Functions that cannot be formulated in this language are not computable.
The language and the
axiom became known as the
(pronounced: ``lambda'') calculus. Gerald Sussman and Guy L. Steele Jr.
later based Scheme on the
calculus. In the mid-1970s,
Gordon Plotkin
suggested the
axiom as a better method for understanding function
applications in programming languages such as Scheme.