Assignment

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.


Exercise 1

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.


Exercise 2

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.


Exercise 3

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.


Exercise 4

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

Exercise 5

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.


Exercise 6

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.


Exercise 7

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

John David Stone (stone@cs.grinnell.edu)