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 let-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.
Chez Scheme 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 Chez Scheme starts up; definitions add new
identifiers. A procedure call 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. Similarly, when a let-expression is invoked,
an entry is added to the environment for each of the identifiers in its
binding list, but all such entries are removed as soon as the body of the
let-expression has been completely evaluated.
It is possible to introduce a binding for an identifier that is already
bound. If the new binding is created by a procedure call or a
let-expression, 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 call or inside the body of the
let-expression, the new binding takes precedence over the old
one, since its card is on top; but when the procedure returns or the
let-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 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 the binding lists of let-expressions
are local variables. (A 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 let-expression.)
However, Scheme provides a way to assign a new value to any bound variable,
whether it was originally bound by a definition, a procedure call, or a
let-expression. 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 an assignment expression is evaluated, the new-value
expression is evaluated first, and then the current binding for the
variable is changed so that the variable is bound to the new value. (The
old value written on the topmost card for that variable is erased and the
new value is written in instead.)
It is an error 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. (Chez Scheme will step in and create a global variable for you if you commit this error, but most implementations of Scheme are not so considerate.)
> (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
For the following exercises, define a global variable named
*counter*, giving it the initial value 0:
(define *counter* 0)
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
Assignment can be used to simplify the coding of some otherwise tricky
procedures. For example, the tally-by-parity procedure posed
as an exercise back in the lab on recursion
is perhaps a little easier to understand if assignment rather than
rebinding is used to adjust the values of the tally variables:
(define tally-by-parity
(lambda (ls)
;; Test the precondition.
(if (not ((list-of integer?) ls))
(error 'tally-by-parity "The argument must be a list"))
;; Set up two tally variables, one for odd integers, the other for even
;; ones.
(let ((odds 0)
(evens 0))
;; Run through the elements of the list.
(let loop ((rest ls))
;; If you're at the end of the list, return a list of the final
;; tallies.
(if (null? rest)
(list odds evens)
;; Otherwise, mutate the appropriate tally variable, then
;; recurse.
(begin
(if (odd? (car rest))
(set! odds (+ 1 odds)) ; Add 1 to ODDS.
(set! evens (+ 1 evens))) ; Add 1 to EVENS.
(loop (cdr rest))))))))
Revise the tally-by-parity procedure so that it tallies
positive and negative real numbers rather than odd and even integers.
Zeroes should not be counted either as positive or as negative.
Here is a procedure that opens a file that is assumed to contain only numbers, reads them in, computes their average, closes the file, and returns the average:
(define average-of-file
(lambda (filename)
;; Test the precondition.
(if (not (string? filename))
(error 'average-of-file "The file name must be a string"))
;; Open the file and initialize the running total and the tally of
;; numbers read.
(let ((source (open-input-file filename))
(sum 0)
(count 0))
;; Run through the file, reading in values one by one.
(let loop ((next (read source)))
;; At the end of the file, close it and return the average (unless
;; there were no numbers; signal an error in that case).
(cond ((eof-object? next)
(close-input-port source)
(if (zero? count)
(error 'average-of-file "The file must not be empty")
(/ sum count)))
;; If the value read in is a number, add it to the running
;; total, add 1 to the tally, and recurse.
((number? next)
(set! sum (+ sum next))
(set! count (+ count 1))
(loop (read source)))
;; If not, signal an error.
(else
(error 'average-of-file
"The file must contain only numbers")))))))
Write a procedure file-size that takes the name of a file
as its argument, opens the file, reads through it to determine how many
characters it contains and how many of those characters are newlines,
closes the file, and writes out (to standard output) a report of the totals
it came up with:
> (file-size "vector-generator.ss") Characters: 1532 Lines: 46
The file-size procedure should be invoked only for its side
effect (the output).
Suppose that you wanted to revise file-size so that it
also reported the number of words and the number of sentences in the given
file. What changes would you have to make? Why is this a significantly
more difficult problem that counting the number of lines?
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/spring-1998/assignment.html
created April 16, 1997
last revised June 21, 1998