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 bindingexpression, 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:
> (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))
2: second binding
3: third binding
4: fourth binding
5: fifth binding
4: fourth binding
3: third binding
2: second binding
> 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.
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.)
> (define ch #\A)
> ch
#\A
> (define ch #\B)
> ch
#\B
> (set! ch #\C)
> ch
#\C
> (set! ch #\D)
> ch
#\D
> (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))
0: E
1: F
2: G
3: r
4: I'm tired of this game.
> ch
#\D
The last example two examples show that assignments to a local variable have no effect on a global variable, even one that happens to have the same name. The erasures occur on the card that is paper-clipped to the top of the pile rather than on the card underneath it.
In preparation for this exercise and the ones that follow it, define a
global variable named *counter*, giving it the initial value
0. (When a Scheme programmer expects that the value of a global variable
will be changed by assignments, she conventionally gives it a name
beginning and ending with a star. This is not a requirement of the
language, but a convention that makes programs more readable.)
Write a procedure named bump-counter that takes no
arguments and is called for its side effect, which is to increase the value
of *counter* by 1.
Write a procedure named reset-counter that takes no
arguments and is called for its side effect, which is to change the value
of *counter* to 0.
Write a procedure named report-counter that takes no
arguments and returns the current value of *counter*.
> (bump-counter) > (report-counter) 1 > (bump-counter) > (bump-counter) > (bump-counter) > (report-counter) 4 > (reset-counter) > (report-counter) 0
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:
(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 of expression evaluation 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 it 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,
(define vector-sum
(lambda (vec)
(do ((remaining (vector-length vec) (- remaining 1))
(total 0 (+ total (vector-ref vec (- remaining 1)))))
((zero? remaining) total))))
(Note that 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, like bump-counter and reset-counter
above, or a procedure that has exclusive control over some variable that
retains its value between calls (what is sometimes called a static
variable).
As an example of a procedure that has exclusive control over a static variable, here's one that acts like a light switch:
(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.
Define a procedure named invocations that takes no arguments
and returns a natural number indicating the number of times it has been
invoked. (Use a static variable to keep track of this number.)
> (invocations) 1 > (invocations) 2 > (invocations) 3 > (invocations) 4
Define a procedure named parm that takes either one argument
or none and controls one static variable. Given no arguments,
parm should return the value of its static variable; given one
argument, it should assign the argument to the static variable, returning
an unspecified value.
Define a unary procedure named list-keeper. Given any
argument other than the symbol ':report,
list-keeper should return the symbol 'ok. When
the argument is ':report, however, list-keeper
should return a list of the arguments from all of the invocations of it
since the last one with the ':report argument:
> (list-keeper ':report) () > (list-keeper 'start) ok > (list-keeper 'next) ok > (list-keeper 2) ok > (list-keeper "three") ok > (list-keeper 'finish) ok > (list-keeper ':report) (start next 2 "three" finish) > (list-keeper 'start-again) ok > (list-keeper 'keep-going) ok > (list-keeper ':report) (start-again keep-going)
Hint: Save the arguments of the non-report calls in a list that is the value of a static variable.
Design and write a procedure direction-counter that takes one
argument, which must be one of the five symbols 'north,
'east, 'south, 'west, or
':report. Given any of the first four symbols, it should
return the symbol 'ok; given the symbol ':report,
it should display an appropriately formatted summary of the number of times
it has been invoked with each argument since the last report.
> (direction-counter ':report) north 0 east 0 south 0 west 0 > (direction-counter 'east) ok > (direction-counter 'east) ok > (direction-counter 'south) ok > (direction-counter 'east) ok > (direction-counter 'north) ok > (direction-counter 'south) ok > (direction-counter 'east) ok > (direction-counter ':report) north 1 east 4 south 2 west 0
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/scheme/assignment.xhtml
created April 16, 1997
last revised April 25, 2000