Although it is sometimes convenient to think of a binding as a relationship between an identifier and a value, this conception of bindings in Scheme is actually not quite correct. A Scheme identifier is actually bound to a location -- a region in the memory of a computer in which one can store a value.
As we have seen, one can bind a variable to a value by defining it, or by
invoking a procedure in which the variable is a parameter, or by placing a
binding specification for it in a binding expression (that is, a let-, let*-, letrec-, named let-, or do-expression). Creating a binding in any of these ways is like writing
the variable and its value on, say, a three-by-five card and filing it away
in a box of similar cards. DrScheme maintains an internal table of
variables and values that is similar to such a card box; it's called an
environment.
Predefined identifiers like number? and <= are already in the
environment when DrScheme starts up. There are several ways of extending
this initial environment, however: (1) Top-level definitions add new
identifiers to the environment. (2) Invoking a procedure adds an entry to
the environment for each of the parameters of the procedure, but these
entries are only temporary; when the procedure returns, the cards that were
added are removed from the box and thrown out. (3) Similarly, when a
binding expression or an internal definition is encountered, an entry is
added to the environment for each of the identifiers that acquires a value;
but all such entries are removed as soon as the binding expression or the
expression enclosing an internal definition has been completely evaluated.
It is possible, by these mechanisms, to introduce a binding for an identifier that is already bound. If the new binding is created by a procedure invocation, a binding expression, or an internal definition, what happens is that in effect the card containing the new value is paper-clipped to the front of the card for the old binding. During the procedure invocation or inside the body of the binding expression, the new binding takes precedence over the old one, since its card is on top; but when the procedure returns a value or the evaluation of the binding expression is over, the top card is removed and thrown away, and the old binding is still in place and in force. For example:
> (define str "original binding") > str "original binding" > (let ((str "new binding")) str) "new binding" > str "original binding"
In fact, the same identifier can be repeatedly rebound in this way, with the clipped-together stack of cards getting thicker and thicker; none of the bindings will be lost, and each will reappear after the ones on top of it have been removed. Consider the following expression:
(let ((str "second binding"))
(display "2: ") (display str) (newline)
(let ((str "third binding"))
(display "3: ") (display str) (newline)
(let ((str "fourth binding"))
(display "4: ") (display str) (newline)
(let ((str "fifth binding"))
(display "5: ") (display str) (newline))
(display "4: ") (display str) (newline))
(display "3: ") (display str) (newline))
(display "2: ") (display str) (newline))
At levels 2, 3, and 4, the value of the current binding is displayed, then the variable is rebound at the next higher level, and finally the current binding is uncovered and displayed again. Here is the output:
2: second binding 3: third binding 4: fourth binding 5: fifth binding 4: fourth binding 3: third binding 2: second binding
None of this rebinding has any effect the top-level binding of str:
> str "original binding"
However, if you use a top-level definition to override a previous definition, the effect is somewhat different. You should think of the new value in such a case as replacing or overwriting the old one, as if the value printed on the three-by-five card containing the old binding were erased and a new value written in on the same card. After such a redefinition, there is no way to recover the old value, and at the implementation level it is quite possible that the memory location that it used to occupy is now occupied instead by the new value. In short, redefinition has the effect of assigning a new value to an existing variable, rather than creating a second binding for that variable.
In recognition of this difference in the way the bindings are treated, variables that are either predefined or bound by top-level definitions are sometimes called global variables, while procedure parameters and variables that appear in binding expressions are local variables. The rationale for the name is that the top-level redefinition of a variable changes its value ``globally'' -- through all subsequent uses of that binding -- whereas other rebindings change the value only ``locally,'' within a procedure body or the body of a binding expression.
set!-expressions
However, Scheme provides a way to assign a new value to any bound
variable, no matter how the binding was originally created. A set!-expression (sometimes called an assignment expression)
consists of a pair of parentheses enclosing the keyword set!, the
variable whose value is to be changed, and an expression that provides its
new value. When the set!-expression is evaluated, in effect, the
old value written on the topmost card for that variable is erased and the
new value is written in instead.
Here's an example of a set!-expression:
(set! power (expt base 4))
This expression cannot be evaluated unless power and base are
already bound, in the evaluation environment. Its effect is to overwrite
the previous value of power with the value of the expression (expt base 4). As the presence of the exclamation point suggests, this
operation is destructive and irreversible; unless the previous value
associated with power is also stored somewhere else, it's gone for
good.
The mechanics of assignment are not really like those of vector-set!
and the other mutation procedures that we have discussed up to this point.
Set! is a keyword, not the name of a procedure, and set!-expressions are not procedure calls. Assignment is not a way of
changing the contents of a container while keeping the same container; it
is a way of manipulating the binding of a previously bound variable.
It is an error, therefore, to assign a new value to a variable that is not bound at all -- if there is no card in the box for a certain variable, there's nothing to erase. (Some implementations of Scheme will step in and create a global variable for you if you commit this error, but DrScheme prohibits it.)
The target of a set!-expression can be either a global variable or a
local one. When the variable is global, a set!-expression works
just like a redefinition:
> (define ch #\A) > ch #\A > (define ch #\B) > ch #\B > (set! ch #\C) > ch #\C > (set! ch #\D) > ch #\D
However, assignment to a local variable is quite different in effect from rebinding. Consider this expression:
(let ((ch #\E)) (display "0: ") (display ch) (newline) (set! ch #\F) (display "1: ") (display ch) (newline) (set! ch #\G) (display "2: ") (display ch) (newline) (set! ch (integer->char 114)) (display "3: ") (display ch) (newline) (set! ch "I'm tired of this game.") (display "4: ") (display ch) (newline))
Here the same variable is bound to the same location throughout, but the
set!-expressions keep changing the contents of that location. Here
is the output that is generated when the expression is evaluated:
0: E 1: F 2: G 3: r 4: I'm tired of this game.
Assignments to a local variable have no effect on a global variable, even one that happens to have the same name:
> ch #\D
The erasures occur on the card that is paper-clipped to the top of the pile rather than on the card underneath it.
In many programming languages, assignment expressions are used quite frequently to tweak the values of local variables. For example, a procedure to compute and return the sum of a vector of numbers might take this form:
;;; vector-sum: compute the sum of the elements of a vector of numbers ;; Given: ;; VEC, a vector of numbers ;; Result: ;; TOTAL, a number ;; Preconditions: ;; None. ;; Postcondition: ;; TOTAL is the sum of the elements of VEC. (define vector-sum (lambda (vec) (let ((size (vector-length vec)) (position 0) (total 0)) (do () ((= position size) total) (set! total (+ total (vector-ref vec position))) (set! position (+ position 1))))))
In this approach, the storage locations denoted by position and
total are fixed, and neither of these identifiers is rebound inside
the do-expression. Instead, the values stored in those locations
are repeatedly overwritten with new values as the vector is traversed.
Because assignment is a side effect, the programmer who uses it has to be
much more careful about the order in which expressions are evaluated than a
Scheme programmer working in a functional style. For example, it would be
an error to reverse the order of the two set!-expressions in the
body of the preceding definition, because then the procedure would
incorrectly use the new rather than the old value of position when
indexing into the vector.
In this case, using assignment is merely a poor stylistic alternative to
rebinding; it would be better to write vector-sum as, say,
;;; vector-sum: compute the sum of the elements of a vector of numbers ;; Given: ;; VEC, a vector of numbers ;; Result: ;; TOTAL, a number ;; Preconditions: ;; None. ;; Postcondition: ;; TOTAL is the sum of the elements of VEC. (define vector-sum (lambda (vec) (do ((remaining (vector-length vec) (- remaining 1)) (total 0 (+ total (vector-ref vec (- remaining 1))))) ((zero? remaining) total))))
Notice that in this case the two iteration specifications could be reversed without changing the effect of the procedure.
The cases in which you really need assignment are those in which you want either a procedure that has a lasting side effect on a global variable, or a procedure that has exclusive control over some variable that retains its value between calls (what is sometimes called a static variable).
Procedures that destructively change the values of global variables raise no new issues; the first four exercises in the lab on assignment illustrate how it's done. As an example of a procedure that has exclusive control over a static variable, here's one that acts like a light switch:
;;; light-switch: toggle the state of a static switch and return an ;;; indication of its new state ;; Givens: ;; None. ;; Result: ;; STATE, a symbol. ;; Preconditions: ;; None. ;; Postconditions: ;; STATE is ON if the LIGHT-SWITCH procedure has been invoked an odd ;; number of times, OFF if it has been invoked an even number of times. (define light-switch (let ((lit #f)) (lambda () (set! lit (not lit)) (if lit 'on 'off))))
In English: Initially, let lit be false, and create a procedure that
takes no arguments. When invoked, the procedure applies the not
procedure to lit to get the negation of its value, makes this
negated value the new value of lit, and returns 'on or 'off, depending on whether the new value of lit is true or false.
The result is that on successive invocations light-switch
returns different values:
> (light-switch) on > (light-switch) off > (light-switch) on > (light-switch) off > (light-switch) on
Because the let-expression encloses the lambda-expression in
the definition of light-switch, the static variable lit is
created and initialized only once, when the light-switch procedure
is defined, and it retains its value between calls to light-switch.
(If we placed the let-expression inside the lambda-expression, a new local variable lit would be created and
initialized every time light-switch was invoked, so the state of the
switch would not be carried over from one invocation to another.)
It is impossible for any procedure other than light-switch to affect
the value of lit in any way, since the identifier lit is not
bound to the relevant storage location except in the body of the let-expression that introduces it. This is what it means to say that
light-switch has exclusive control over the static variable lit: The programmer can be certain that the only way to manipulate the
variable is by invoking the procedure.