Scheme and The Art of Programming
Scheme and The Art of Programming
^of Programming
Daniel P. Friedman
'^d
Part 1
Data
eration. Here we combine the various data types into different structures and
characterize the procedures that process them.
We introduce local variables in Chapter 5. In order to package privately
computational objects that work together, we introduce lexical scope.
Finally, in Chapter 6, we show some of the advantages of displaying infor-
mation and entering data while a computation proceeds.
Data
Data and Operators
. . .it is not the thing done or made which is beautiful, but the doing. If we
appreciate the thing, it is because we relive the heady freedom of making it.
Computing is an art form. Some programs are elegant, som,e are exquisite,
some are sparkling. My claim, is that it is possible to write grand programs,
noble programs, truly magnificent program,s.
Donald E. Knuth,
from an article by William Marling
in Case Alumnus
1.1 Introduction
It is a craft. A
program made with craftsmanship is both more serviceable
and more satisfying. Programming requires proficiency born of practice (hence
the many exercises in this book!). It requires great dexterity, though of a
mental rather than a manual sort. As woodworkers enjoy working with their
hands and fine tools, so programmers enjoy exercising their minds and working
with a fine programming language.
It is an art. Fine programs are the result of more than routine engineering.
They require a refined intuition, based on a sense of style and aesthetics that
is both personal and practical. As an artistic medium, programming is highly
plastic, unconstrained by physical reality. In programming, perhaps more
than in other arts, less is more. Simplicity is nowhere more practical than in
programming, where the bane is complexity. When just the right abstraction
for a problem has been found, it may be a thing of beauty. We hope you take
pleasure in the programs of this book.
It is not a science, but it is based on one: computer science.Though
our primary concern in this book is with the techniques of programming, we
will have occasion to introduce a number of important scientific results. We
hope you find the language and style of this book to be vehicles for deeper
understanding and appreciation throughout your study of computer science.
It is a literary endeavor. Of course, programs must be understood by
computers, which requires mastery of a programming language. But that
is not enough! Programs must be analyzed to understand their behavior,
and most programs must be modified periodically to accommodate changing
needs. Thus it is essential that programs be intelligible by humans as well as
by computers. The challenge is to convey the necessary details without losing
sight of the overall structure. This in turn requires creative use of abstrac-
this book.
But this book is ultimately about more than the craft of engineering artistic
and literate programs. Programming teaches an algorithmic (step by well-
specified step) approach to problem solving, which in turn encourages an
algorithmic approach to gaining knowledge. This view of the world is pro-
viding numerous fresh insights in fields as diverse as mathematics, biology,
and sociology, as well as providing tools that assist and extend our minds in
almost every field of study. Thus programming ability, like mathematical and
writing ability, is an asset of universal value.
Programming ability and literary ability have another thing in common.
An essay or short story can be correct grammatically and can convey the
information that the author intended and still not be a literary work of art.
Computer programming has its own aesthetic, and good programmers strive to
The following two publications are manuals for Scheme that accompany the
implementations of Scheme on microcomputers:
We encourage you to read them because each presents its own programming
philosophy. We are all using the same language, but we have somewhat dif-
As you read these pages, remember that you should care how elegant your
programs are. The task that confronts you is not only to learn a programming
language but to learn to think as a computer scientist and develop an aesthetic
about computer programs. Enjoy this as an opportunity to understand the
creative process better. Solve problems not only for their solutions but also
for an understanding of how the solutions were obtained.
1. The input deuce, in this case the keyboard with the standard typewriter
keys and some additional ones. Each key can perform several functions.
On both the typewriter and the computer keyboard, we choose between
lower and upper case by depressing the Shift key. On the computer, we
can also hold down the Control (CTRL) key while pressing another key to
get another behavior, and on some computers, we can similarly hold down
the Alternate (ALT) key while pressing another key to get yet another
behavior. Finally, pressing and releasing the Escape (ESC) key before
pressing another key gives still another behavior. When a key is pressed,
the result is usually shown on the screen.
2. The processor, in which the computing is done. This contains the internal
memory of the computer, the arithmetic logic unit, and the registers where
the computations take place.
3. The output devices: the video monitor on which the interactive computing
is viewed, which we refer to as the screen, and the printer where printed
copy of the output is produced.
generally use the letters of the alphabet, the digits from through 9, and some
of the other characters on the keyboard. A few of the other characters on the
keyboard have special meaning, just as certain characters like the period and
comma have special meaning in English. In Scheme, the characters
()[]{};."''# \
have special meaning and cannot appear in symbols. Similarly, the characters
+ - .
are used in numbers and may occur anywhere in a symbol except as the first
In much the same way, symbols may be assigned some meaning in Scheme. A
symbol used to represent some value is called a variable. The computer must
determine the meaning of each variable or number it is given. It recognizes
that the numbers have their usual meaning. Scheme also keeps the meaning
of certain variables that have been assigned values, and when it is given a
symbol, it checks to see if it is one of those that has been kept. If so, it can
use that meaning. Otherwise it tells us that the symbol has not yet been
given a meaning.
To carry the analogy with the English language a step further, words are
put together in sentences to express the thoughts you want to convey. The
Scheme analog of a sentence is an expression. An expression may consist of
a single symbol or number (or certain other items to be defined later), or a
list, which is defined to consist of a left parenthesis, followed by expressions
separated by blank spaces, and ending with a right parenthesis. We first
appears on the screen, prompting you to enter something. The nature of the
prompt depends on the implementation you are using. The prompt we use
in this book to simulate the output on the screen is a pair of square brackets
surrounding a number. Thus the first prompt will be
[1]
If you type a number after theprompt and then press the <RETURN> key
(sometimes referred to as the <ENTER> key),
[1] 7 <RETURN>
Scheme recognizes that the meaning of the character that you typed is the
number 7. We number 7
say that the value of the character you typed is the
or that what you type has been evaluated to give the number 7. Scheme then
writes the value of what you type at the beginning of the next line and moves
down one more line and prints the next prompt:
[1] 7 <RETURN>
7
[2]
Let us review what we have just seen. At the first prompt, you enter 7 and
press <RETURN>. In general, an expression (or a collection of such expressions)
you enter in response to the prompt and before pressing <RETURN> is called
a program. In this example. Scheme reads your program, evaluates it to the
number 7, prints the value 7 on the screen at the beginning of the next line,
and then prints the next prompt one line lower. Thus Scheme does three
things in succession: it reads, it evaluates, and it prints. We refer to this
In this example, Scheme evaluates the third subexpression, which has the value
10, assigns that value to the symbol ten, and finally, in our implementation
of Scheme, prints the next prompt. Since the value returned by a define
expression is never used, that value is not prescribed in the specification of
the language. For convenience in writing this book, we opt to suppress the
value returned by a define expression.
Now let's see what happens when we enter the symbol ten:
This time, Scheme successfully evaluates the variable ten, so it prints the
value 10.
We have seen that a variable is a symbol to which a meaning (i.e., a value)
can be given. When a value is we say that the variable is
given to a variable,
bound to that value. In our previous example, the symbol ten is a variable
bound to the value 10. In general, if var represents a variable and expr
represents an expression whose value we would like to bind or assign to var,
we accomplish the assignment by writing
(quote symbol)
For example, you quote the symbol ten by writing (quote ten). If you type
(quote ten) and then <RETURN> in response to a Scheme prompt, you see
From now on, we shall omit the <RETURN> notation. It is understood that
each line that we type must be followed by <RETURN>. We use the word enter
when we want to indicate that something is to be typed in response to the
Scheme prompt. The value that Scheme prints in response to what we enter is
said to be the value that the expression "evaluates to" or that is "returned."
For example, we could have said, "If the symbol ten is bound to 10, and you
enter (quote ten), then Scheme evaluates it to ten, while if you enter ten,
Scheme evaluates it to 10."
In all cases, whether a symbol is bound to some value or not, when a
quoted symbol is entered, the literal symbol is returned. Thus if we enter
(quote abc3). Scheme returns abc3. It is not necessary to quote numbers,
for the value of a number as an expression is the number itself.
We can also assign to a variable a value that is the literal value of a symbol.
For example, if we enter the following:^
we bind the variable Robert to the symbol Bob. When we next enter Robert,
we get
[9] Robert
Bob
so that Scheme has evaluated Robert and returned the value Bob.
We have two types of data so far,
numbers and symbols. How are they used?
The use of numbers should be no surprise, since we usually think of doing
arithmetic operations on numbers to get answers to problems. We shall take
a brief look at how we do arithmetic in Scheme in this section and then return
for a more complete look at using numbers in Chapter 3. To perform the
arithmetic operations on numbers, Scheme uses prefix notation; the arithmetic
operator is placed to the left of the numbers it operates on. The numbers on
which it operates are called the operands of the operator. Furthermore, the
operator and its operands are enclosed in a pair of parentheses. Thus to add
the two numbers 3 and 4, we enter (+3 4) and Scheme evaluates it and
returns the answer 7. On the computer screen it looks like this:
[10] (+ 3 4)
7
^ We are mixing lower and uppercase letters in ovir symbols and showing that Scheme
returns the same mix of lower and uppercase letters as their literal values. Thus, if we
enter 'Bob, Scheme retvims Bob. An implementation of Scheme that preserves the case of
letters is called case preserving, and in this book, we are assuming that the implementation
is case preserving. There are some implementations that are not case preserving, which
means that the case is changed to either all lowercase or all uppercase letters. Thus, in
some implementations, all letters are returned in lowercase, and when we enter Bob, Scheme '
evaluates it to bob. Other implementations that are not case preserving return all uppercase
letters, so that if we enter 'Bob, Scheme evaluates it to BOB.
[11] (* 3 (- 12 5))
21
[12] (+ 2 (/ 30 15))
4
In general, Scheme uses this prefix notation whenever it applies any kind
of operator to its operands. We shall return to a more complete discussion
of numerical computations in Chapter 3. A number of experiments with
numerical operations are included in the exercises at the end of this section.
Insummary, a symbol can be bound to a value using a special form that
begins with the keyword define. When a variable that has been bound to
a value is entered in response to a Scheme prompt, its value is returned. If
we want Scheme to return the literal value of the symbol instead of the value
to which it is bound, we quote the variable. The value of a quoted symbol is
just the literal value of the symbol.
It is possible to keep a record of the session you have in Scheme. The
particular mechanism for doing so depends on the implementation of Scheme
you are using. If you are using a version of Scheme that uses the windowing
capability of the computer, you may be able to send what is in the window
to a file. In some implementations, it is possible to run Scheme in an editor
and use the saving capability of the editor to preserve what you want from
the session in a file. Some versions offer a transcript facility that you turn
on at the beginning of the session and give it a filename, and then turn off
at the end of the session. The session is then preserved in the named file.
The manual for Scheme you are using should identify the facility you have
the
available to save your Scheme sessions.
We strongly recommend that you try each of the things discussed in this
book at the computer to see how they work. Feel free to experiment with
variations on these ideas or anything else that occurs to you. You get a
much better feeling for computers and for Scheme if you "play around" at the
keyboard.
Exercises
Exercise 1.1
Find out what method your implementation of Scheme has for recording your
Scheme session in a file. Bring up Scheme on the computer and record this
session in a file called "sessionl.rec." Enter each of the following to successive
prompts: 15, -200, 12345678901234, (quote alphabet -soup), 'alphabet-
soup, '
'alphabet-soup. (Note: Experiment with entering even larger posi-
tive and negative whole numbers and decimals and see what is returned.)
Exercise 1.2
Assume that the following definitions have been made in the given order:
What values are returned when the following are entered in response to the
prompt?
a. big-number b. small-number
c. 'big-niunber d. Cheshire
e. 'Cheshire f. number 1
g. number h. ' number
Conduct the experiment on the computer in order to verify your answers.
Exercise 1.3
What is the result of entering each of the following expressions in response to
the Scheme prompt? Verify your answer by performing these experiments on
the computer.
a. (- 10 (- 8 (- 6 4)))
b. (/ 40 (* 5 20))
c. (/ 2 3)
Exercise I.4
Write the Scheme expressions that denote the same calculation as the following
arithmetic expressions. Verify your answers by conducting the appropriate
experiment on the computer.
b. (3x(4 + (-5--3)))
c. (2.5^(5 X (1^10)))
Exercise 1.5
If a, /?, and 7 are any three numbers, translate each of the following Scheme
expressions into the usual arithmetical expressions. For example:
a. (+ a (- (+ /? 7) a))
b. (+ (* a /?) (* 7 /?))
c. (/ (- a /?) (- Q 7))
So we have seen two data types, symbols and numbers. Another important
far,
data type in Scheme is lists. We all use lists in our daily lives shopping lists, —
laundry lists, address lists, menus, schedules, and so forth. In computing, it
We see from this example that we enclosed three things in parentheses: the
variable cons, the number 1, and the empty list '(). The first entry tells us
The define expression we entered at the prompt [2] binds the variable Isl
to the value obtained by evaluating the subexpression (cons 1 '()). That
subexpression evaluates to the list (1). Thus Isl is bound to the list (1).
Thus when the variable Isl is entered at the prompt [3], its value (1) is
returned.
We now create a list with 2 as its first element and the elements of Isl as
the rest of its elements. To accomplish this we write
Once again, the two operands are evaluated —2 evaluates to itself and Isl
to the list ( 1 ) . Then a new list is formed having 2 as its first item and the
items of Isl as the rest of its items, giving us (2 1) . This is the value that is
returned. At prompt [5], we verify that Isl is unchanged. Let us next bind
the variable ls2 to a list like the one in [4]
first element and the elements of ls2 as the rest of its elements by writing
When we apply cons to its two operands, the operands are both evaluated.
The first operand, c, evaluates to three, and the second operand, ls2, evalu-
ates to the list (2 1). Then a new list is built with three as its first item and
the elements of the list (2 1) as the rest of its elements. The value (three
2 1) is returned.
Continuing our experiment, we bind the variable ls3 to the value of (cons
c ls2) using a define expression:
We now perform another experiment with cons. Let us build a list that
has as its first item the list ls2 and as the rest of its items the same items
as those in ls3. This is done by making ls2 the first operand and ls3 the
second operand of cons:
[13] ls3
(three 2 1)
The first operand of cons evaluates to the list (2 1), and the second operand
of cons evaluates to (three 2 1). Thus the procedure cons produces a new
list that has as its first item the list (2 1). followed by the elements in IsS.
This gives us the value that was returned by Scheme: ((2 1) three 2 1).
Notice that when ls3 was entered in response to the prompt [13] . (three 2
1), the original value of ls3, was returned, so cons did build a new list and
did not affect the list ls3.
We now in a position to summarize the facts that we observed in the
are
experiments. The procedure cons takes two operands. We apply the proce-
dure cons to these operands by enclosing the procedure name cons followed
by its two operands in parentheses. In general, a procedure name followed by
its operands, all enclosed in a pair of parentheses, is called an application, and
evaluated by Scheme, all of the expressions in the list are evaluated in some
unspecified order. The value of the first expression (the operator) informs
Scheme of the kind of computation that is to be made (in our case, cons in-
forms Scheme that a list is to be constructed). Then the computation defined
by the procedure (the value of the operator) is performed on the arguments,
which are the values of the operands. We assume for now that the second
operand of cons evaluates to a list (which may be the empty list). Then a
new list is created containing the value of the first operand as its first item
followed by all of the items in the list to which the second operand evaluated.
It is this new list that is returned as the value of the application. Since cons
first evaluates its operands, the lists contain only values. So far, these values
may be numbers, the literal value of symbols, and lists of these items. As
we progress through the chapters of this book, we shall encounter other data
types, all of which can be included in lists.
We have assumed in the discussion that the second operand of the cons
application evaluates to a list. This is the usual situation that we shall en-
counter, but it is also possible for the second argument to cons not to be a
list. We shall discuss this case in the next section. Furthermore, we see in ls4
that a list may contain in it other lists. We say that the inner list is nested
within the outer list. The nesting may be several levels deep, for a nested
list may itself contain nested lists. Suppose we have a given list. Items that
are not nested within lists contained in the given list are called the top-level
items of the given list. Thus, if the given list is ((a b (c d)) e (f g) h),
the top-level items are the list (a b (c d)), the symbol e, the list (1 g),
and the symbol h.
We can also build the list (2 1) in one step by applying cons twice as the
next experiment illustrates:
[16] (cons (cons 2 (cons 1 '())) (cons 'three (cons 2 (cons 1 '()))))
((2 1) three 2 1)
The second and third cons's build the list (2 1), and the fourth, fifth, and
sixth build the list (three 2 1). Then the first cons constructs the desired
list.
then evaluates the expression according to what the first item tells it to do.
[17] (2 1)
Exercises
Exercise 1.6
Using the symbols one and two and the procedure cons, we can construct the
list (one two) by typing (cons 'one (cons 'two '())). Using the symbols
one, two, three, and four and the procedure cons, construct the following
lists without using quoted lists (you may use quoted symbols and the empty
list):
e. (((one)))
Exercise 1.1
Consider a list Is containing n values. If a evaluates to any value, how many
values does the list obtained by evaluating (cons a Is) contain?
Exercise 1.8
What is the result of evaluating '(a 'b)? (Try it!) Explain this result.
We have seen how to build lists using the constructor cons. We now consider
how to take a list apart so that we can manipulate the pieces separately and
build new lists from old. We accomplish this decomposition of lists using two
and cdr take one operand that must evaluate to a nonempty list. Both car
and cdr are not defined on an empty list, and applying them to an empty list
produces an error.
Let's look at the behavior of the selector catr. When its argument is a
nonempty list, it returns the first top-level item in the list. Thus we have
The double arrow "^^" is read as "evaluates to" or "returns." Here are some
other examples of applying the procedure car (As in the previous section, ls4
is bound to the list ((2 1) three 2 1).)
(car * (a b c d)) =^ a
(car ls4) => (2 1)
(car '((1) (2) (3) (4)))=> (1)
(car '(ab (cd ef) gh)) => ab
(car '(((hen cow pig)))) ^^ ((hen cow pig))
(car •(())) =>
the list returned is obtained when the first item (the caur) of the eirgument
list is removed. Thus
' The symbol cdr is pronoiinced "coiild-er." The nsmies car and cdr had their origin in the
way the hst-processing language LISP wa^ originally implemented on the IBM 704, where
one could reference the "address" and "decrement" parts of a memory location. Thus car
is an acronym for "contents of address register," and cdr is an acronym for "contents of
decrement register."
For the next example, let list-of-names be bound to the list ((Jane Doe)
(John Jones)). We look at how we retrieve Jane Doe's last name from this
list. If we first apply car to list-of -names, we get the list (Jeme Doe). We
now get the list (Doe) by applying cdr, and finally, we get Doe by applying
car. We want to emphasize the distinction between the list (Doe) containing
one item and the item Doe itself. All of these steps are combined in the
following expression:
In this example, we see that the procedures car and cdr are applied in
We now have the two items that will make up our final list. We use cons to
build the final answer. We first use cons to build a list around the list (ice
cream) to get ((ice cream)) and then use cons again to build a list that
has (chicken soup) as its first item and (ice cream) as its second item.
The shown here can be used to build and manipulate lists in just
process
about any way we want. As we learn more about Scheme, we shall discover
shortcuts that facilitate the manipulation of lists.
Exercise
Exercise 1.9
If a and /? evaluate to any values, what is
After all of these operations involving car, cdr, and cons, the values of the
operands a and Is-b stayed the same when they were entered in [6] and [7]
as they were when they were defined in the beginning.
So we have encountered three procedures car, cdr, and cons that
far, — —
help us manipulate lists and four procedures +, *, -, and / —
that allow us to —
operate on numbers. Another group of procedures, called predicates, applies
a test to their arguments and returns true or false depending on whether the
test is passed. Scheme uses #t to denote true and #f to denote false. The
value of #t is #t and the value of #f is #f so both of these are constants.* #t
,
and #f representing true and false, are known as boolean (or logical) values.
,
They form a separate type of data to give us five distinct types: numbers,
symbols, booleans, pairs (including lists), and procedures. More data types
will be introduced in later chapters. We now look at several predicates that
apply to these five data types.
The first predicate tests whether its argument is a number, and its name is
number?. Like most other predicates, the name ends with a question mark,
signaling that the procedure is a predicate. Thus if we apply the predicate
number? to some object, #t is returned if the object is a number, and otherwise
#f is returned. If we make the following definitions,
(number? -45.67) =» #t
(number? '3) =* #t
(number? num) =^ #t
(number? twelve) ^^ #f
(number? 'twelve) ==* #f
(number? (+2 3)) => #t
(number? #t) ==* #f
(number? (car '(15.3 -31.7))) =» #t
(number? (cdr '(15.3 -31.7))) => #f
In the last example, the operand evaluates to (-31.7), which is a list, not a
number.
The predicate symbol? tests whether its argument is a symbol. With the
definitions of num and twelve given above, we get the following results:
A pair is an object built by the constructor cons, and the predicate pair?
tests whether its argument is a pair. For example, nonempty lists are con-
structed by cons, so they are pairs. We have
There is also a predicate null? which tests whether its argument is the
empty list.
(null? '()) =* #t
(null? (cdr '(cat))) => «t
(null? (car '((ab)))) => #f
Exercises
Exercise 1.10
If the operands oc and evaluate to any values, what is
Exercise 1.11
If a list Is contains only one item, what is (null? (cdr Is) )?
argument is a procedure.
(= 3 (/ 6 2)) =^ #t
(= (/ 12 2) (* 2 3)) => »t
(= (car '(-1 ten 543)) (/ -20 (• 4 5))) => #t
(= (* 2 100) 20) ==> #f
There is also a predicate eq? to test the sameness of symbols. If its operands
same symbol, #t is returned. For
evaluate to the this example, assume that
Garfield has been bound to 'cat.
The predicate eq? returns #t if its two arguments are identical in all re-
spects; otherwise it returns #f . Symbols have the property that they are
identical if they are written with the same characters in the same order. Thus
we use eq? to test for the sameness of symbols. On the other hand, each appli-
cation of cons constructs a new and distinct pair. Two pairs constructed with
separate applications of cons will always test #f using eq? even if the pairs
they produce look alike. For example, let us make the following definitions:
Then we have
by the cons in [2] , which is distinct from that built in [1] , so eq? again
evaluates to #f . Finally, Is-c is defined to be the value of Is-a, which is the
pair built by the cons in [1] , so both Is-a and Is-c refer to the same pair,
objects the predicate tests for sameness, we use the predicate eqv?. We shall
later see that eqv? also tests vectors, strings, and characters for sameness.
We have not included lists among the data types we can test for sameness
using the predicates discussed. If we want a universal sameness predicate
that can be applied to test numbers, symbols, booleans, procedures, and lists
(and strings, characters, and vectors), we use the predicate equal?. In the
case of pairs constructed using separate applications of cons, equal? tests the
corresponding entries, and if they are the same, #t is returned. Thus equal?
tells us that the two lists (cons 'a '(be d)) and (cons 'a '(be d))are
the same, whereas eq? and eqv? claim that they are diff"erent.
Now for the obvious question: How do we know which one to use? When
a predicate must first test to determine the type of its arguments, it is less
efficient than one designed specifically for the type of its arguments. Thus for
numbers, = is the most efficient sameness predicate. Similarly, for symbols,
eq? is the most efficient predicate. For testing only numbers or symbols, eqv?
is more efficient than equal?. When we know that we shall be using numbers
or symbols, then eqv? is the sameness predicate we use. When the discussion
is limited to numbers, we use =.
evaluated and the value 1 is returned and printed on the screen. On the other
hand, not every Scheme object is printable. If we enter only the name of a
procedure, such as car, the procedure, which is the value of car, is returned,
but not printed; instead a message is displayed, which indicates a procedure.
In this book, we indicate a procedure by printing angle brackets surrounding
the name of the procedure in italics. Thus, when we enter car, <car> is
The number of items in (cons a /?) is one greater than the number of
items in /?.
We have been introduced to five basic data types (numbers, symbols, bool-
and procedures), and we have seen a number of procedures to
eans, pairs,
manipulate and test the data. In Chapter 2 we shall develop the tools to
compute with lists, and in Chapter 3 we shall do the same for numbers.
Exercises
Exercise 1.12
Evaluate each of the following.
Exercise 1.13
We can extract the symbol a from the list (b (a c) d) using car and cdr
by going through the following steps:
For each of the following lists, write the expression using car and cdr that
extracts the symbol a:
a. (b c a d)
b. ((b a) (c d))
c. ((d c) (a) b)
d. (((a)))
Exercise 1.1
Decide whether the following expressions are true or false:
Exercise 1.15
Decide whether the following expressions are true or false:
2.1 Overview
2.2 Procedures
^ Speciad forms look like applications but are not, and in order to recognize them, we have
to memorize the keywords, such as lambda and define. We shadl see other keywords later,
but the list of keywords we have to memorize is small.
of the lambda expression is evaluated with this parameter binding. The value
of the body is returned as the value of the application of the procedure.
The lambda expression has the syntax
The keyword lambda is followed by a list that contains the parameters. The
ellipsis (three dots) following parameter indicates that the list contains zero
or more parameters. The next subexpression is the body of the lambda expres-
sion. The value of a lambda expression is the procedure, which can be applied
to values appropriate for the evaluation of the body. These values must agree
in number with the number of parameters in the lambda expression's param-
eter list. When the procedure is applied, the parameters are bound to the
corresponding values, and the body is evaluated. The value of the body is
then the value of the application.
In general, when a procedure is applied, the syntax is
(operator operand . . .
Thus to apply the procedure we defined above to build a list containing the
symbol bit, we enter
each time we want to apply the procedure. We can avoid this by giving the
procedure a name and using that name in the procedure applications. This
2.2 Procedures S3
is done by choosing a name, say maike-list-of-one, for this procedure and
then defining make-list-of-one to have the desired procedure as its value.
We write
This is easier to read if we display the parts more clearly on separate lines as
follows:
(define make-list-of-one
(leimbda (item)
(cons item '())))
Scheme ignores any spaces in excess of the one space needed to separate
expressions. Scheme also treats <RETURN>'s as spaces until the one following
the last right parenthesis that is entered to close the first left parenthesis in
the expression. Thus Scheme reads the two ways of writing this definition
of make-list-of-one as the same Scheme expression. The indentation sets
off subexpressions, making the structure of the program easier to understand
(make-list-of-one 'bit)
to read.
A word is in order about the choice of names for procedures and parameters.
Since a symbol can have as many characters in it as we wish, programs will be
easier to read if we choose names that describe the procedure or parameter.
Now let's write a procedure called make-list-of-two that takes two ar-
guments and returns a list whose elements are those two arguments. The
definition is:
The parameter list following the keyword leiinbda consists of two parameters,
iteml and item2. You may be wondering about the semicolons in the first and
second lines of the program and the statements following them. When Scheme
reads an expression, it ignores all semicolons and whatever follows them on a
line. This allows us to make remarks about the program so that the reader
looking at it will know the intent of the programmer. Such remarks are called
documeniaiion and can make understanding programs easier. By choosing
the names of variables carefully, you can reduce the amount of documentation
necessary to understand a program. The documentation can also precede or
follow the program if each line is preceded by a semicolon. In the programs
in this book, we try to select variable names that make such documentation
unnecessary. When we wish to make points of clarification, we shall state
them in the accompanying discussion.
We apply the procedure maike-list-of-two to the two symbols one and
two by writing
two arguments were the values of the operands 'one and 'two.
In Section 1.5, we saw how to take a list containing four items (menu was
bound to the list (chicken soup ice cream)) and build a new list containing
the same items but grouped into two lists, each containing two items. We can
use the procedure make-list-of-two to give us another way of doing that
grouping. We define a procedure called regroup that has as its parameter
list-of-4, which will be bound to a list of four items. It returns a list with
the items in list-of-4 regrouped into two lists of two items each. In the
2.2 Procedures 35
course of writing the definition of regroup, we shall find it clearer to make
use of certain other procedures, which express what we want to appear in the
list of the two items we create. We use these procedures in the definition of
regroup and then define them afterward. The order in which the definitions
are written does not matter, and it is often more convenient to use a procedure
in a definition where it is needed, and then to define it later. In the definition
that follows, we make use of two such helping procedures, first-group and
second-group.
(define regroup
(lambda (list-of-4)
(mcike-list-of-two
(first-group list-of-4)
(second-group li8t-of-4))))
(define first-group
(lambda (Is)
(meike-list-of-two (car Is) (cadr Is))))
(define second-group
(lambda (Is)
(cddr Is)))
the list of four items and the helping procedure make-list-of-two is applied
to build the desired list consisting of the first two items in the list of four
the list of four items following the first two, that is, the list consisting of the
There are two styles of writing programs, top-down and bottom-up program-
ming. In both, we are looking for the solution of some problem and want to
write a procedure that returns the desired solution as its value. For now, we
refer to this as themain procedure. In top-down style, we first write the defi-
nition of themain procedure. The main procedure often uses certain helping
procedures, so we write the definitions of the helping procedures next. These
in turn may require other helping procedures, so we write those, and so on. In
bottom-up style, we first write the definitions of the helping procedures that
we anticipate using, and at the end, we write the main procedure. We shall
use both styles of programming in this book.
We summarize this discussion by observing that the value of a lambda
expression with the syntax
isa procedure. The ellipsis after parameter means that this is a list of zero or
more parameters. When the procedure is applied, the parameters are bound
to the arguments (i.e., the values of the operands), and the body is evaluated.
2.2 Procedures 37
)
We can give the procedure a name by using a define expression with the
structure
{procedure-name operand . . .
where the number of operands matches the number of parameters in the def-
inition of the procedure. In general, when an application of the form
{operator operand . . . )
is evaluated, the operands and the operator are all evaluated in some un-
specified order. The operator must evaluate to a procedure. The values of
the operands are the arguments. The procedure binds the parameters to the
arguments and evaluates the body, the value of which is the value of the ap-
plication. Because the operands are first evaluated and it is their values, the
arguments, that the procedure receives, we say the operands are passed by
value to the procedure.
We have also encountered two expressions that are called special forms:
those with the keywords define and lambda. These expressions are not ap-
plications because not all the items in the expressions are evaluated initially.
For example, in a lambda expression, the parameter list is never evaluated
and its body is not evaluated initially. Most computer languages have some
keywords that have special meaning and cannot be used for other purposes.
In Scheme the number of such keywords for special forms is relatively small.
In Chapter 14, we shall see how we can add to Scheme our own special forms.
Exercises
When doing these exercises, you may find it convenient to save the defini-
tions of the procedures in a file. These procedures can then be used again.
They can be entered into Scheme from a file in which they were saved either
by using a transfer mechanism or by invoking a loading procedure. In some
implementations of Scheme, this is done with (load "filename'')
(define f irsts-of-both
third, and the third element becomes the first. Test your procedure on:
2.2 Procedures 39
Exercise 2.5: switch
Define a procedure switch that interchanges the and third elements of a
first
three-element list. Test your procedure on the examples given in the previous
exercise.
This description of the procedure using English gives a sequence of steps that
we follow to carry out the computation. Such a sequence of steps describing a
computation is called an algorithm. We
implement the kind of "case analysis"
given in tl s algorithm using a cond expression (the special form with keyword
cond). The keyword cond is derived from the word conditional. Using cond,
we write a procedure called type-ol that tests its argument and returns the
type of the item as described above:
(define type-of
(leUDbda (item)
(cond
((pair? item) 'pair)
((null? item) 'empty-list)
((number? item) 'niimber)
((symbol? item) 'symbol)
(else 'some-other-type))))
Let us analyze the cond expression. In this case, the cond expression has
five clauses, each represented by two expressions enclosed in parentheses. The
first clause, ( (pair? item) 'pair), has as its first expression (pair? item),
which is a boolean or logical expression with the value #t or #f depending on
whether the value bound to item is or is not a pair. We shall also refer to the
first expression, and if all of the preceding conditions are false, the expression
following else is evaluated, and its value is returned. The expression following
else is referred to as the alternative.
In general, the syntax of a cond expression is
(cond
{conditiorii consequenti)
{condition2 con3equent2)
iconditioun consequentn)
(else alternative))
Scheme has another way of handling conditional expressions that have only
two cases. We can also use the special form with keyword if. Suppose we
want to write a procedure car-if-pair that does the following:
(define car-if-pair
(lambda (item)
(cond
((pair? item) (car item))
(else item))))
* The else clause is optional. Hit is omitted and all of the conditions are false, then Scheme
does not specify the value that is returned as the value of the cond expression. We shall
avoid using cond expressions that return unspecified values.
(define ceur-if-pair
(leunbda (item)
(if (pair? item)
(c2u: item)
item)))
or
(define type-of
(lambda (item)
(if (pair? item)
'pair
(if (null? item)
'empty-list
(if (niimber? item)
'number
(if (symbol? item)
'symbol
' some-other-type) ) ) ) )
Any cond expression can be written as nested if expressions, but as the num-
ber of cases increases, the nesting of the if expressions gets deeper, and the
meaning of the whole conditional expression is obscured. Thus, using a cond
expression is often advantageous when there are several cases.
The use of conditional expressions with either if or cond depends upon first
exprn in succession. If any one of them is false, it stops evaluating the rest
of the subexpressions, and the value of the and expression is #f . If all of the
subexpressions have true values, the value of the last subexpression is returned
as the value of the and expression.^
The or expression evaluates each of the subexpressions expri, expr2, ,
exprn in succession. If any one of them is true, it stops evaluating the rest
of the subexpressions, and the value of the or expression is the value of that
first true subexpression. If all of the subexpressions are false, the value of the
or expression is #f
The value of the not expression is #1 when expr has a true value, and it is
(define s-eind-n-list?
(lambda (Is)
(euid (pair? Is)
(symbol? (car Is))
(pair? (cdr Is))
(number? (cadr Is)))))
^ Scheme has a convention of treating einy value that is not false as true. Thus (if 'cat
'kitten 'puppy) ^^ kitten, since the condition 'cat evaluates to cat, which is not false.
It is good programming style, however, for the conditions to be boolean expressions that
evaduate to either tt or tf
some-list is a pair,
and the first item in some-list is a symbol,
and the cdr of some-list is a pair,
and the second item in some-list is a number.
while
(s-and-n-list? '(a b D) =» #f
The test to determine whether the list is a pair is necessary since we can only
take the Ceo: of a pair. If the list is empty, the evaluation of the car of the
list never takes place. The evaluation terminates on the first false value.
(define s-or-n-list?
(lambda (Is)
(and (pair? Is)
(or (symbol? (cau: Is))
(number? (car Is))))))
some-list is a pair,
and either the first item in some-list is a symbol or it is a number.
(define singleton-list?
(lambda (Is)
(and (pair? Is) (null? (cdr Is)))))
Exercises
Exercise 2.6
Assume that a, b, and c are expressions that evaluate to #t and that e and f
are expressions that evaluate to #f. Decide whether the following expressions
are true or false.
Exercise 2.1
Decide whether the following expressions are true or false if expr is some
boolean expression.
Exercise 2.8
Decide whether the following expressions are true or false using s-cind-n-
list? as defined in this section.
d. (s-and-n-list? '(a))
Exercise 2.9
Decide whether the following expressions are true or false using s-or-n-list?
as defined in this section.
a. (s-or-n-list? '(b))
d. (s-or-n-list? '())
2.4 Recursion
We saw in Section 2.2 that certain procedures use other procedures as helping
(last-itea '(12345)) =* 5
It is a good idea to begin with the simplest cases of the arguments to which
the procedure is applied. In this case, the simplest nonempty list is a list
containing only one item. For example, if the list is (a), then the last item is
also the first item, and applying cair to this list produces the last item. This
would work with any list containing only one top-level item, for the car of the
list is both its first and its last toi>-level item. Let us use the variable Is as
the parameter in the definition of last-item. How can we test whether Is
contains only one top-level item? When Is contains only one top-level item,
its cdr empty list. Thus the boolean expression (null? (cdr Is))
is the
returns #t when — —
and indeed only when the nonempty list Is contains only
one top-level item. Thus, we may use a cond expression to test whether we
have the case of a one-item list and return the car of the list if that is the
case. We can then begin our program as follows:
(define last-item
(lambda (Is)
(cond
((null? (cdr Is)) (car Is))
... )))
obtained by evaluating (cdr Is),^ which contains all of the items of Is except
its first item. The last item in (cdr Is) is the same as the last item in Is. For
example, the list (a b c) and the list (b c), which is its cdr, have the same
Icist item, c. Thus if we call the procedure last-item as a helping procedure
to be applied to (cdr Is), we get the desired last item of the original list,
and that solves our problem. Thus to complete the definition of last-item,
we add the else clause to handle the case where the list contains more than
one item:
(define last-item
(lambda (Is)
(cond
((null? (cdr Is)) (car Is))
(else (last-item (cdr 1 s))))))
To see that this docs define the procedure last-item so that it returns the
correct result for any nonempty list Is, we consider first a list (a) containing
only one item. Then the condition in the first cond clause is true, and (ceir
Is) does give us the last (which is also the first) item, a, in the list. Thus last-
item works on any list containing only one item. Now let's consider the case
in which Is is a list (a b) containing two items. Then its cdr, (b), contains
one item, so the procedure last-item does work on (cdr Is), allowing us to
use it as a helping procedure in the else clause to get the correct result. Thus
last-item solves the problem for any list of two items. Now we use the fact
that last-item works on the cdr of any three-item list to conclude that it
^ It is common practice, when the context is clear, not to include the phraise obtained by
evaluating. We say, "the hst (cdr Is)" instead of "the list obteuned by evaJuating (cdr
Is)" whenever the context makes it clear that we want the value of (cdr Is) rather thain
the litereJ hst whose first iten> is cdr euid whose second item is Is. When we weint the
Utereil list, eind the context is not cleair, we indicate so by quoting it.
2.4 Recursion 4^
—
works on the three-item list itself. We can continue this process of increasing
by one the number of items in the list indefinitely, showing that last-item
solves the problem for any list.
the list consisting of only one item. The simplifying operation is cdr, and in
the other cases, we see that the expression that solves the problem applies
last-item to the simplified list (cdr Is).
To give us a better intuition about how last-item works, we shall apply
last-item to the list (a b c). What is (last-item '(a b c))? We shall
walk through the evaluation of this expression. The parameter Is is bound
to the argument (a b c), and the cond expression is evaluated. In this case,
(cdr Is) is not empty, so the alternative in the else clause is evaluated.
This tells last-item to (cdr Is). Since (cdr Is) is (b c),
us to apply
we must evaluate (last-item (b c)). We thus bind the parameter Is to
'
the argument (be) and enter the cond expression. Once again, (cdr Is)
is not empty, so we evaluate the alternative in the else clause. This tells us
to apply last-item to (cdr Is), which now is (c). Thus we must evaluate
(last-item '
(c)). We now bind the parameter Is to the argument (c) and
enter the cond expression. This time (cdr '(c)) is the empty list. Thus the
consequent is evaluated to give (car '
(c) ) c as the value of the expression.
The recursion in the illustration stops when the list is simplified to the
base case. In that case, the condition in the first cond clause is true. We
call the condition used to stop the recursion the ierminaUng condition. In
our example, the terminating condition is (null? (cdr Is)). Generally,
whenever a recursive procedure is defined, a terminating condition must be
included so that the recursion will eventually stop. (In Chapter 15 on streams,
we shall see examples in which a terminating condition is not needed.) We
usually begin the definition of a recursive procedure by writing the terminating
condition as the first cond clause. We then proceed with the rest of the
definition.
argument is equal? to one of the top-level items in the list that is its second
argument. For example,
we use the predicate null? so the terminating condition is (null? Is). The
consequent for the terminating condition is #f . We can therefore begin the
definition of member? as a procedure having two parameters, item and Is:
(define member?
(leunbda (item Is)
(cond
((null? Is) «f)
... )))
Now given any list, what is the simplifying operation that simplifies Is to
the empty list? It is again the procedure cdr. Assume that Is is not empty.
If we know the value of (member? item (cdr Is) ) how do we get the value ,
for (member? item Is)? Well, when is the latter statement true? It is true
if either the first item in Is is the same as item or if item is a member of
the rest of the list following the first item. This can be written as the or
expression:
Thus in the case when Is is not empty, the above expression is true exactly
2.4 Recursion 4^
when the expression (member? item Is) is true. We then complete the defi-
nition of member? with
(define member?
(lambda (item Is)
(cond
((null? Is) «f)
(else (or (equal? (car Is) item)
(member' item (cdr Is)))))))
'
The procedure member? is recursive since it calls itself. Let us review the
reasoning used in the program for member?. If the terminating condition
(null? Is) is true, then item is not in Is, and the consequent is false. Oth-
erwise we look at the alternative, which is true if either item is the first item
in Is or if item is in (cdr Is) and is otherwise false.
When member? calls itself with argument (cdr Is), its parameter is bound
to the value of (cdr Is), which is a shorter list than the parameter's previous
binding to Is. In each successive recursive procedure call, the list is shorter,
and the process is guaranteed to stop because of the terminating condition
(null? Is).
In order to use a list as the first argument to member? (as in Example 4),
we used the predicate equaJ.? to make the sameness test in the else clause. If
we know that the items to which item is bound will always be symbols, we
can use eq? in place of equsd.?. The procedure so defined using eq? is named
memq? to distinguish it from member?, which is defined using equatl? for the
sameness test. Similarly, if we know that the items to which item is bound
will always be either symbols or numbers, we can use eqv? for the sameness
level item out of a list, and the procedure member?, which tests whether ein
^ Scheme provides the three procedures member, nemq, and memv, written without the ques-
tion mark. These behave somewhat differently from the ones we defined with the question
mark in that if item is not found, false is returned, but if item is found in Is, the subUst
whose car is item is returned. For example, (memq 'b '(a b c)) ^^ (b c).
listwe build is still the empty list. The test for the base case is then (null?
Is), and the value returned in its consequent is (). Thus the definition of the
procedure remove-lst begins with
(define remove-lst
(Isunbda (item Is)
(cond
((null? Is) '())
... )))
If Is is not empty, the procedure that simplifies it to the base case is again
cdr. If we already know (remove-lst item (cdr Is)), that is, if we have
a list first top-level occurrence of item removed from (cdr
consisting of the
Is), how do we build up a list that is obtained by removing the first top-level
occurrence of item in Is? There are two cases to consider. Let's first consider
the example in which we remove the first occurrence of a from the list (a b
c d). Since a is the first item in the list, we get the desired result by merely
taking the cdr of the original list. This is the first case we consider. If the first
top-level item in Is is the same as item, then we get the desired list by simply
using (cdr Is). This case can be added to the definition of remove-lst by
writing
2.4 Recursion 51
(define reaove-lst
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
... )))
The only case left to be considered is when Is is not empty, and its first
top-level item is not the same as item. Consider the example in which we
apply remove-lst to remove the letter c from the list (a b c d). The list
is not empty and its first item is not c. Thus the list we build begins with
a and continues with the items in (b d). But (b d) is just the list obtained
by removing c from (b c d). The final result is then (a b d). which was
obtained by building the list
In general, the list we are building now begins with the first element of Is
and has in it the elements of (cdr Is) with the first top-level occurrence of
item removed. But this is obtained when we cons* (car Is) onto (remove-
lst item (cdr Is) ) , so the final case is disposed of by adding the else clause
to the definition, which is given in Program 2.4.
(define remove-lst
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
(else (cons (car Is) (remove-lst item (cdr Is)))))))
* Scheme programmers use the verb cons, which has am infinitive "to cons", tenses "cons,
cons'd, has cons'd" participle "consing"
, , and conjugation "I cons, he conses, etc." We shall
make frequent use of these words.
To get the value of this expression, we must evaluate the remove-lst subex-
pression. Once again, the list (b c d) is not empty, and the first item in the
list is not the same as c. Thus the alternative in the else clause is evaluated.
This gives us as the value of the whole expression above:
Once again, to get the value of this expression, we must evaluate the remove-
lst subexpression. Now the list (c d) is not empty, but its first item is the
same as c. Thus the condition in the second cond clause in the definition of
remove-lst is true and the value of its consequent is (d). Thus the above
expression has the value
(a b d)
This is the value returned by the procedure call. In the next section, we shall
see how the computer can help us walk through a procedure application.
In order to be able to remove a sublist from a given list, as in Example
5, the predicate equal? was used to test for sameness in the second cond
clause. Ifwe know that all of the arguments to which item will be bound are
symbols, we can use eq? to test for sameness. The procedure defined using eq?
instead of equal? is named remq-lst. Similarly, if we restrict the arguments
to which item will be bound to symbols or numbers, we can use eqv? to test
for sameness in the second cond clause, and we name the procedure so defined
remv-lst.
Exercises
Exercise 2.10
Rewrite the definitions of the three procedures last-item, member? and
remove-lst with the cond expression replaced by if expressions.
2.4 Recursion 53
)
Exercise 2.11
The definition of member? given in this section uses an or expression in the
else clause. Rewrite the definition of member? so that each of the two subex-
pressions of the or expression is handled in a separate cond clause. Compare
the resulting definition with the definition of remove-lst.
Exercise 2.12
The following procedure, named mystery, takes as its argument a list that
contains at least two top-level items.
(define mystery
(Icunbda (Is)
(if (null? (cddr Is))
(cons (car Is) '
)
top-level occurrence of the item old in Is and replaces it with the item new.
Test your procedure on:
(define insert-right-lst
(lambda (new old Is)
(cond
((null? Is) '())
((equal? (car Is) old)
(cons old (cons new (cdr Is))))
(else (cons (car Is)
(insert-right-lst new old (cdr Is)))))))
composed of nonempty lists of items. Its value is a list composed of the first
2.4 Recursion 55
Exercise 2.16: replace
Define a procedure replace that replaces each top-level item in a list of items
Is by a given item new-item. Test your procedure on:
technically known, trace our programs. We can also use this tool to find and
correct errors in our programs, a process called debugging.
The computer can help us walk through or trace our programs if we make
use of a procedure writeln (read as "write-line") that prints its arguments
directly to the computer screen.Some Scheme implementations provide the
procedure writeln, and if make it available,
the one you are using does not
you can enter its simple definition.® The procedure writeln takes any number
of arguments. When we evaluate
® A more
complete discussion of writeln and related procedures that write to the screen
is 7. You may enter the definition of writeln given in Program 7.5 if
presented in Chapter
your implementation of Scheme does not provide it.
the expressions expri expr2 . . exprn are all evaluated; then their values are
printed on the screen in order from left to right with no blank spaces between
them. When the last value is moves to the beginning of
printed, the cursor
the next line. Like every other procedure, writeln must return a value, but
we are not concerned with this value. In fact, different implementations of
Scheme may return different values. Since it is unspecified in Scheme what
value writeln returns, we shall assume in our implementation that the value
returned is not printed on the screen.
For example, if the variable Jack is bound to the value Jill and the variable
Punch is bound to the value Judy, the evaluation of (writeln Punch Jack)
will print
JudyJill
on the screen with no space between the words. If we evaluate the expression
(writeln 'Punch 'Jack), then the screen shows
PunchJack
We can control the spacing and print sentences on the screen if we use
another type of data called strings. A string is any sequence of keyboard
characters. In Scheme, a string is written as a sequence of characters enclosed
with double quotes: ". Thus "This is a string. " is an example of a string.
then
This is a string.
appears on the screen. Note that the double quotes are not printed with the
string. Thus the evaluation of the expression
^° A cheu-acter, such as a backslash, which is used to change the normal meaning of what
follows it is referred to €is an escape character.
prints
He said "Hello".
If we evaluate
evaluated consecutively in the order that it appears and the value of the last
[2] (begin
(writeln "The remove-lst expression")
(writeln "is applied to the list (1 2 3 4)")
(writeln "to build a new list without the number 2.")
(remove-lst 2 '(1 2 3 4)))
The remove-lst expression
is applied to the list (12 3 4)
to build a new list without the number 2.
(1 3 4)
We want to stress that what is printed on the screen is not the value of the
writeln expressions. Instead, what is printed on the screen is done as a side
effect. A side effect causes some change to take place (in this case, the change
was printing on the screen), but it is not a value that is returned. When using
a begin expression, all of the subexpressions before the last one are included
for their side effects and not for the values that they return. The value of
the last subexpression is the only one returned. Here is another example to
illustrate that only the value of the last subexpression is returned.
[3] (begin
(+ 3 4)
(- 5 11)
(* 10 10))
100
The values of the first two subexpressions are ignored. In this case, the first
two subexpressions did not produce any side effects, so although they were
evaluated,we do not see any evidence of it and there really was no point in
where the expressions expri, expr2, ... exprn are evaluated in their given
order, and the value of the last one, expTn, is returned.
We now have all the tools we need to use writeln to help us walk through
an application of remove- 1st to remove the letter c from the list (a b c
When we enter a cond clause, the condition is the entering expression whose
parameter test is bound to the value of the original condition of remove-lst.
If test is true, it writes the fact that we are entering the cond clause with
the appropriate identifying number and the current value of the variable Is.
In any event, test is returned as the value of the condition. If test is false,
the next cond clause is entered. If test is true, the consequent of that cond
clause is evaluated. If the else clause is entered, we use the quoted symbol
else as the first argument of entering. Scheme treats the symbol else as
true (since it is not false) so the alternative is evaluated.
The consequent (or alternative) in each cond clause of remove-lst-trace
is a leaving expression. It has the value of the original consequent (or alter-
native) of the cond clause of remove-lst as the binding of its first parameter,
result. When the leaving expression is evaluated, it tells us the identifying
number of the cond clause and the value to which result is bound. It then
returns result.
Now let's apply remove-lst-trace to see how this tracing information
helps us see what is happening during the evaluation.
This output tells us that we first entered the third cond clause with Is bound
to (a b c d). With this binding, the leaving expression in the alternative is
(define remove-lst-trace
(lambda (item Is)
(cond
((entering (null? Is) Is 1)
(leaving '()!))
((entering (equal? (car Is) item) Is 2)
(leaving (cdr Is) 2))
((entering 'else Is 3)
(leaving
(cons (car Is) (remove-lst-trace item (cdr Is)))
3)))))
(define entering
(lambda (test input cond- clause -number)
(begin
(if test (writeln " Entering cond-clause-"
cond-clause-number " with Is = " input))
test)))
(define leaving
(leuobda (result cond-clause-number)
(begin
(writeln "Leaving cond-clause-"
cond-clause-number " with result = " result)
result)))
is evaluated, and b is waiting to be consed onto its value before we can leave
cond clause 3. As before, remove-lst-trace is called again before the leaving
writeln expression is evaluated. This time, the first item in (c d) is same
the
as and we are told that we entered the second cond clause
c, with Is bound
to (c d). When we enter the consequent, the first operand in the leaving
expression evaluates to (d). Then the writeln expression prints on the screen
that we are leaving the second cond clause with the result bound to (d),
and the value (d) is returned.
Cons expression (2) is waiting for the value of the remove-lst-trace call,
and now that value is (d) . With this value, the cons expression in (2) evaluates
to (b d). We can now complete the evaluation of the leaving expression,
which tells us that we are leaving cond clause 3 with result bound to (b
d). But this is just the value that cons expression (1) is waiting for as the
value of remove-lst-trace invocation. Using the value (b d) as its last
its
argument, cons expression (1) evaluates to (a b d). It was the first operand
in the application of leaving in the third cond clause. Now that it has
been evaluated, the writeln expression writes its message, which says that
we are leaving cond clause 3 with result bound to (a b d). The leaving
invocation now returns the value to which result is bound, (a b d), and
that becomes the value of the original procedure call. The trace we made
here illustrates well the order in which we enter and leave the cond clauses.
We see that we do not leave the cond clause until a value is found for the
recursive invocation of remove-lst-trace, and the evaluation of the cons
expression can be completed.
In the previous example,we entered only the second and third cond clauses.
If we invoke remove-lst-trace to remove an item from a list that does not
contain it, we enter only the first and third cond clauses, eis the following trace
illustrates:
Analyze the trace to be sure you can explain it in a manner similar to that
used in the previous example.
We have used writeln expressions to trace a program by printing certain
information about places in the program where the evaluation is being made
and the values of certain variables at that place. This helps us understand
how programs work. It is also an excellent tool for finding errors in programs.
If a program is not doing what you expect it to do, you can put a writeln
expression at certain places in the program where you think the error may be
and look compare them with what you expect at
at the values of variables to
that place. By studying these values, you can frequently pinpoint the source
of the error and make the appropriate changes to cause the program to work
correctly. When the program is corrected and runs as you want, the writeln
expressions used to locate the errors should be removed. Tracing a program
with the writeln expressions placed at strategic points is a helpful and often
used debugging tool.
Exercise
Exercise 2.22
In the first trace, the second and third cond clauses were entered. In the
second trace, the first and third cond clauses were entered. Can you give
a remove-lst-trace invocation that enters only the first and second cond
clauses? Explain.
In the fourth example, the a and b in the list are not at top level, so they are
not swapped.
In order to define swapper, we begin with an analysis of the base case.
What is the simplest case for this problem? If Is is empty, there is nothing
to swap and the empty list is returned. Thus we take cis the base case for Is
the empty list, and we begin the definition as follows:
(define swapper
(lambda (x y Is)
(cond
((null? Is) '())
... )))
A nonempty list is simplified to the base case using the simplifying oper-
ation cdr. What is returned if we invoke (swapper x y (cdr Is))? The
(cdr Is) with the items x and y interchanged. But this differs
result will be
from (snapper x y Is) only in that the first item in (swapper x y Is) is
missing. We will get (swapper x y Is) from (swapper x y (cdr Is)) by
consing the correct first item onto (swapper x y (cdr Is)). There are three
possibilities for this first item: it can be x, y, or neither. First, if (car Is) is
X, we should cons y onto (swapper x y (cdr Is)), so the next cond clause
in our definition can be added:
(define swapper ,
(leunbda (x y Is)
(cond
((null? Is) '())
((equal? (car Is) x)
(cons y (swapper x y (cdr Is))))
... )))
(define swapper
(lanbda (x y Is)
(cond
((null? Is) '())
((equal? (car Is) x)
(cons y (swapper X y (cdr Is))))
((equal? (car Is) y)
(cons X (swapper X y (cdr Is))))
(else
(cons (car Is) (swapper x y (cdr Is)))))))
(define swapper
(lambda (x y Is)
(cond
((null? Is) '())
((equal? (car Is) x)
(cons y (swapper x y (cdr Is))))
((equal? (car Is) y)
(cons X (swapper x y (cdr Is))))
... )))
Finally, if (car Is) is neither x nor y, then we just cons (ceir Is) itself
onto (swapper x y (cdr Is)), giving us the else clause and completing the
definition given in Program 2.8.
If we invoke the procedure swapper with the arguments 'b, 'd, and '(a
b c d b). it should return the list (a d c b d) in which b and d have been
interchanged. Let's walk through the program to see how it constructs this
answer. In the first procedure call, Is is bound to (a b c d b). This list is
not empty, and its car is neither b nor d, so the else clause is evaluated and
gives as the answer the cons expression:
Let's refer to the value of this cons expression as answer- J , and that is the
value that we are looking for to solve the problem. At this point, however.
consequent gives us
and we give the invocation of swapper within answer-3 the name answer-4-
This gives us the return table:
answer-1 is: (cons 'a answer-2)
answer-2 is: (cons 'd answer-3)
answer-3 is: (cons 'c answer-^)
answer-4 is: (swapper 'b 'd ' (d b))
We have added answer-3 to our return table to wait until we have the value
of answer-4 •
Once again we have added answer-5 to the return table to wait until we
have a value for answer-6. In the invocation of swapper in answer-6, the
terminating condition in the first cond clause is true, and the value () is
the values of each of the waiting variables in our return table. Until now,
on each recursive invocation of swapper, a new row was added to the return
table waiting for a value. This time we got a value for answer-6 so we do ,
not have to add a row to the return table. Instead we replace the swapper
expression in the last row by its value ( ) . We can now work our way back up
the table one row at a time, replacing each variable on the right side by the
value it has on the next row below. We shall write these replacements in a
answer-6 is: ()
answer-5 is: (d)
answer-4 is: (b d)
answer-3 is: ( [c b d)
answer-2 is: (d c b d)
answer- 1 is: (a d c b d)
The last row gives us the anticipated value for our invocation of swapper.
Let's take another look at the definition of the procedure swapper. In the
last three cond clauses, something is consed onto
(define swapper
(lambda (x y Is)
(cond
((null? Is) '())
(else (cons (swap-tester x y (car Is))
(swapper x y (cdr Is)))))))
(define swap-tester
(Icunbda (x y a)
(cond
((equal? a x) y)
((equal? a y) x)
(else a))))
The use of such helping procedures often simplifies the writing and reading of
programs. We shall make frequent use of this technique.
We could also have achieved the same effect without using the helping
procedure swap-tester by using in swapper the cond expression of swap-
tester in place of calling swap-tester. This leads to another version of
swapper:
(define swapper
(lambda (x y Is)
(cond
((null? Is) '())
(else (cons (cond
((equal? (car Is) x) y)
((equal? (car Is) y) x)
(else (car Is)))
(swapper x y (cdr Is)))))))
Exercises
Exercise 2.23
Identify what is printed on the screen and what is returned in each of the
following:
a. (begin
(writeln "(* 3 4) = " (• 3 4))
(= ( 3 4) 12))
b. (begin
(writeln "(cons 'a '(be)) has the value " (cons 'a (b c))) '
c. (begin
(writeln "Hello, how aure you?")
(writeln "Fine, thanX you. How are you? " 'Jack)
(writeln "Just great! It is good to see you again, " 'Jill)
"Good-bye. Have a nice day.")
(define describe
(lambda (s)
(cond
((null? s) (quote '()))
((nunber? s) s)
((synbol? s) (list 'quote s))
((pair? s) (list 'cons (describe (ceir s)) (describe (cdr s))))
(else s))))
a. (describe 347)
b. (describe 'hello)
Exercise 2.25
Write a trace similar to the one used in remove-lst-trace to trace the pro-
cedure swapper, showing the binding of the parameter Is each time the cond
expression is entered and whenever a cond clause is exited. Invoke the traced
procedure swapper-trace on the arguments b, d, and (a b c d b) used in
the example in this section.
Exercise 2.26
In the return table built for the invocation of swapper in this section, the
computation did not stop when the terminating condition was true and the
first cond clause returned ( ) . Instead, the variables in the table were evaluated
one by one until the value of the first was obtained to provide the value of
the original invocation. This program behaved in this way because after each
invocation of swapper, a cons still had to be completed. There was still
c)), and in the second case, consider (member? 'c '(abed)). In these
two examples, there is no procedure waiting to be done after the recursive
Exercise 2.27
Does the answer change if cond clause 2 and cond clause 3 are interchanged
in the definition of swapper? Does the same thing hold if cond clauses 1 and
2 are interchanged in swap-tester?
(define tracing
(lambda (message result)
(begin
(writeln message result)
result)))
(define test-tracing
(lambda (test message input)
(begin
(if test (tracing message input))
test)))
is useful for tracing the test part of a conditional expression. Rewrite the
definition ofremove-lst-trace using test-tracing and tracing instead of
entering and leaving in such a way as to produce exactly the same output
as that generated using entering and leaving.
3.1 Overview
gers are the usual positive and negative counting numbers and zero:
...,-4,-3,-2,-1,0,1,2,3.4,...
where, in this case, the ellipsis means that the list continues indefinitely in
both directions.
The set of real numbers includes both positive and negative decimal num-
bers and zero. For example, 34.56, —3.456, 0.00034, and 17.0 are all real
2.735e2 is the same as 273.5, since the e2 means that 2.735 is multiplied by
—
a one followed by two zeros that is, by 100. Another way of saying this is
that the e2 means that the decimal point is moved two places to the right.
Another example is 2.735e-2, which is the same as 0.02735, since e-2 means
that 2.735 is multiplied by .01, or that the decimal point is moved two places
to the left.
We use two predicates, integer? and real?, to test the type of a number.
The expression (integer? mim) is true if num is an integer and false other-
wise. The expression (real? num) is true if num is any real number, including
integers, and is otherwise false. Three other useful predicates are zero?, pos-
itive?, and negative?, which make the obvious tests to see whether their
arguments are zero, positive, or negative, respectively.
The four basic arithmetic operations are given by the procedures associated
with the variables + for addition, - for subtraction, * for multiplication, and
/ for division. These are applied to numbers with applications, as were the
list operations in Chapter 2. For example, to add the two numbers 5 and 7,
(- 4 32) ==> - 28
(* -15 -3) 45
(/ -15 -3) 5
than entering (+ n 1), we can use the successor procedure addl and write
(addl n). Thus
(addl 7) => 8
(addl -37) =* -36
^ When division is performed with two integers, some implementations of Scheme return
a fraction instead of a decimed. For example, (/ 2 3) is either displayed as 2/3 or as
0.66666666666666.
(define addl
(IckBbda (n)
(+ n 1)))
(define subl
(lembda (n)
(- n 1)))
Similarly the predecessor procedure subl can be used to get the integer
that precedes a given integer. For example, instead of writing (- n 1), we
can write (subl n). Thus
(subl 7) => 6
(subl -37) =^ -38
mation to it, the use of which is made necessary by the way real numbers are
represented in the computer. Thus we would not expect = to return true if
we test (/ 1 3) and 0.333333333333333. In general, because the internal
representation of certain numbers in the computer is only an approximation
to the actual number, we refer to such numbers as inexact numbers. We con-
sider numbers written with explicit use of a decimal point as being inexact,
(abs -5) =^ 5.
e, respectively.
(n in radians).
dividend.
divisor.
2 3 n
Our strategy again is to simplify the problem by reducing the number of terms
n that are being summed. If n is zero, no terms are summed, and the sum is
zero. This will serve as the terminating condition for our recursion.
(define harmonic-sun
(lambda (n)
(cond
((zero? n) 0)
... )))
(define harmonic-sum
(lambda (n)
(cond
((zero? n) 0)
(else (+ (/ 1 n) (harmonic-sum (subl n)))))))
In programs dealing with numbers, it is often the case that the recursion is
(define list-of-zeros
(laabda (n)
(cond
((zero? n) '())
(else (cons (list-of-zeros (subl n) ))))))
The procedure length takes as its argument a list of items Is and then
tells how many top-level items are in the list. For example,
the procedure length, you can use the name =length= when you enter your
definition.
To define length, we use recursion. The base case is the empty list whose
length is zero, and the operation cdr is used to simplify longer lists. We begin
the definition with the terminating condition:
(define length
(laabda (Is)
(if (null? Is)
... )))
Suppose we know (length (cdr Is)); then we get (length Is) by simply
adding one to (length (cdr Is)). This recursive step completes the last
(define length
(lambda (Is)
(if (null? Is)
The number n is called the index of the item extracted from the list Is by
(list-ref Is n). If the index is greater than or equal to the length of the
list of items Is, an error is announced. Since the first item in the list Is has
index 0, we say that the indexing is zero based.
The strategy we use in this recursion is based on the observation that if we
are looking for the nth item in the list, that item becomes the (n — l)st item
in the cdr of the list. Thus we shall successively remove the first item from
the list and simultaneously reduce the index of the desired item by one. If
the index reaches zero and the list is not empty, the first item in the list is
the item returned. We can determine whether the list will not become empty
before or when the index becomes zero by testing whether the length of the
list is larger than the index. If that is not the case, we signal an error. This
enables us to write the first version of the definition of the Scheme procedure
list-ref as follows:
(define list-ref
(lambda (Is n)
(cond
(«= (length Is) n)
(error "list-ref: Index" n "out of range for list" Is))
((zero? n) (car Is))
(else (list-ref (cdr Is) (subl n))))))
In the program for list-ref the test for whether the length of Is is less
,
than or equal to n is made on each recursive call. However, if the test is false
on the first call, it will remain false in each successive recursive call, since
both the length of the and the index are reduced by one in each successive
list
call. It would be a much better program if the test were made only once, and
if it were false, then a helping procedure would be called that produces the
desired item. We give such a definition next.
(define list-ref
(lambda (Is n)
(cond
(«= (length Is) n)
(error "list-ref: Index" n "out of range for list" Is))
(else (list-ref -helper Is n)))))
(define list-ref-helper
(lambda (Is n)
(if (zero? n)
(car Is)
(list-ref-helper (cdr Is) (subl n)))))
is established that the length of the list is greater than the index, the helping
procedure does the rest of the computation to find the item without calling the
procedure list-ref again and determining the length of each Is repeatedly.
Another approach to defining list-ref derives from observing that if, dur-
ing the recursive calls, the list Is becomes empty while the index n is non-
negative, the index must have been too large for the list. Thus the program
can be written as:
(define list-ref
(lambda (Is n)
(cond
((null? Is)
(error "list-ref: Index" n "out of range for list" Is))
((zero? n) (car Is))
(else (list-ref (cdr Is) (subl n))))))
Exercises
ntpl-1 and ntpl-2, as arguments and produces a new n-tuple whose compo-
nents are the sum of the corresponding components of ntpl-1 and ntpl-2.
Test your procedure on:
num and an item a and returns a list of num elements, each of which is a. Test
your procedure on:
(list-front '
(a b c d e f g) 4) =^ (abed)
(list-front (a b c) 4)
' ^^
Error: length of (a b c) is less than 4.
(list-front (a b c d e f g) 0) => ()
'
(multiple? 7 2) => «f
(multiple? 9 3) =^ #t
(multiple? 5 0) =* #f
(multiple? 20) => #t
(multiple? 17 1) => #t
(multiple? 0) => #t
Write a procedure sum-of-odds that sums the first n odd integers. Test your
procedure by evaluating it for all values of n from 1 to 10 to see that each is
Exercise 3.13
If Is is a list of length 1000, how much "cdring" in Is is necessary in each
of the three programs for list-ref presented in this section in order to find
(list-rel Is 4)? Which of the three programs is most efficient?
The numbers discussed above were either integers for which the arithmetic
operations of +, -, and * give exact results or are inexact numbers, which may
be rounded decimal representations and for which the arithmetic operations
give approximations. For division, even if the numerator and denominator
are exact integers, the result may be an approximation; for example, (/ 1 3)
might return the inexact number 0.33333333333333, which is not i. It is
^ Let S = 1 + 3 + 5+ t-(2n-l). We get the szmie sum if we aAd the numbers in reverse
order, so 5 = (2n - 1) + (2n - 3) H h 3 + 1. Adding the first terms of eeich sum, we get
2n. Adding the second terms of each sima, we get 2n and in general adding corresponding
terms in the two svuns, we get the seune siun, 2n. There ese n such corresponding pairs of
terms, so 25 = n(2n) and S = v?
(define rzero?
(lambda (rtl)
(zero? (nuMr rtl))))
Now we recall how two fractions are combined by the various arithmetic
operations. For example, the sum of the fractions j and ^ has the numerator
{a*d) + (fe*c) and the denominator b*d. Thus if x and y are two rational
numbers, we define the sum procedure, say r+, Program 3.9. The first
in
argument to maJte-ratl is the numerator of the sum, and the second argument
to maike-ratl is the denominator of the sum.
(define r+
(laabda (x y)
(ake-ratl
(+ (* (numr x) (denr y)) (* (n\i«r y) (denr x)))
(* (denr x) (denr y)))))
Since the product of two fractions j and | is the fraction having numerator
a*c and denominator b*d, we can define the product procedure r* for rational
numbers as follows:
Progreun 3.10 r*
(define r*
(lasbda (x y)
(eJce-ratl
(* (nxoar x) (numr y))
(* (denr x) (denr y)))))
Program 3.11 r-
(define r-
(laabda (x y)
(ake-ratl
(- (* (n\i«r x) (denr y)) (* (numr y) (denr x)))
(* (denr x) (denr y)))))
r/ in Program 3.13. The definition of r/ reflects the familiar rule, "invert the
divisor and multiply."
Another useful procedure is the predicate r= that tests whether two rational
numbers are equal. Two rationalnumbers j and | are equal if ad = 6c. Thus
we can write the definition of r= given in Program 3.14.
(define rinvert
(lambda (rtl)
(if (rzero? rtl)
"
(error "rinvert: Cannot invert rtl)
(sJce-ratl (denr rtl) (nuar rtl)))))
Program 3.13 r/
(define r/
(laabda (x y)
(r* X (rinvert y))))
Progr€m[i 3.14 r=
(define r«
(laabda (x y)
( ( (niiar x) (denr y)) ( (nuar y) (denr x)))))
(define rpositive?
(laabda (rtl)
(or (and (positive? (niuu: rtl)) (positive? (denr rtl)))
(and (negative? (nuBr rtl)) (negative? (denr rtl))))))
of r>.
Many other familiar procedures can be built up in terms of these, and
we can go on to develop an extensive arithmetic for rational numbers using
(define r>
(lambda (x y)
(rpositive? (r- x y))))
(define BcLX
(lanbda (x y)
(if (> X y)
X
y)))
(define max
(lanbda (x y)
(if (r> X y)
X
y)))
what we have constructed up to this point. For example, we can define the
procedure nneix, which selects the larger of its two arguments, or its second
argument if they are equal. Before defining rmaa, we show in Program 3.17
how Scheme procedure max for two numbers can be defined. Similarly, we
the
can define rmajc as shown in Program 3.18.
The definition of the procedure nnin, which returns the smaller of its two
arguments or the second if they are equal, is obtained from the definition
of nnaix by changing the r> to r<. We are now in a position to make an
important observation. When two procedure definitions are as similar as
nnajc and nnin, we could have written one definition from which both could
be obtained by peissing the predicate r> or r< as an argument to the procedure.
To demonstrate how this is done, let us use the parameter pred to stand for
either of these predicates. Then we define the procedure extreme-vailue in
Program 3.19.
(define rmax
(lambda (x y)
(extreme-value r> x y)))
and
(define rmin
(leunbda (x y)
(extreme-value r< x y)))
We get as a bonus the fact that max and min can also be obtained from
extreme- value, for if x and y are real numbers, we can write
(define msix
(laimbda (x y)
(extreme-value > x y)))
(define min
(lambda (x y)
(extreme- Veil ue < x y)))
(define rprint
(lambda (rtl)
(writeln (nuar rtl) "/" (denr rtl))))
r*, r-, and r/ and the other procedures that we have defined, we can write
many complicated programs using exact arithmetic on rational numbers. If
someone were to give us the three procedures numr, denr, and make-ratl, we
would not have to know how they are defined in order to use them, and other
procedures depending on them, in programs we write.
If. in the course of writing a program, we need a rational number with
numerator 2 and denominator 3, we simply write (make-ratl 2 3) for that
number. Thus we were writing a rational number package for someone else
if
to use, all we would have to provide the user with are the procedures numr,
denr, make-ratl, and the other procedures defined in terms of these and the
user can compute with the package without ever being concerned about how
the procedures numr, denr, and msike-ratl themselves are defined. We have
treated the rational numbers as abstract data. We are now free to choose any
representation of the rational numbers we wish and to define the selectors and
constructor procedures appropriately for the data representation we choose.
What is we are free to change the
especially nice about this approach is that
data representation any time we wish, and when we only redefine the three
procedures numr, denr. and make-ratl, the rest of the procedures we have
written still work and do not have to be changed in any way. That is the
power of abstraction.
So we have been able to write all of our procedures, but we have not
far,
been able to test them because we have not been given the constructor and
the selectors. We have reached the point where we choose a representation
of the rational numbers and define the selector and constructor procedures.
For the firstmethod of defining them, let us take the representation of the
rational number with numerator a and denominator b to be (list a b),
where b is never to be zero. We can then define the selectors numr and denr
for the rational number rtl and the constructor make-ratl for the integers
intl and int2 asshown in Program 3.21. With these definitions, all of the
(define numr
(lambda (rtl)
(car rtl)))
(define denr
(lambda (rtl)
(cadr rtl)))
(define make-ratl
(lambda (intl int2)
(if (zero? int2)
(error "make-ratl: The denominator cannot be zero.")
(list intl int2))))
To find the denominator of a rational number using the given list represen-
tation, we have to take the car of the cdr of the list representing the number.
It is possible to have a representation that makes the denominator operation
more efficient and uses less we use a dotted pair to represent
storage space if
the rational number. Thus the rational number that has numerator a and
denominator b is represented by the dotted pair (a b). Then the selectors
.
are
(define numr
(lambda (rtl)
(car rtl)))
(define denr
(lambda (rtl)
(cdr rtl)))
(define make-ratl
(lambda (intl int2)
(if (zero? int2)
(error "make-ratl: The denominator cannot be zero.")
(cons intl int2))))
Exercises
Use the procedures defined in this chapter for rational arithmetic in defining
(define rpositive?
(lambda (rtl)
(same-sign? (numr rtl) (denr rtl))))
(gcd 8 12) =* 4
(gcd 8 -12) ==^ 4
(gcd 5) ^^ ^ i
a list (p q) in which p/q = a/b and p/q is reduced to lowest terms (so that
1 is the greatest common divisor of p and q) and in which q is positive. Test
your procedure with:
(make-ratl 24 30) =» (4 5)
(make-ratl -10 15) => (-2 3)
(make-ratl 8 -10) =^ (-4 5)
(make-ratl -6 -9) =^ (2 3)
(make-ratl 8) =^ (0 1)
4.1 Overview
In this chapter, we continue our study of recursion over the top-level items in
lists. Then we make the extension to recursion over the items in the nested
sublists as well, giving us tree recursion. In certain of our computations, a
return table is built while operations that have yet to be performed wait for
recursive procedure calls to return values. We discuss another way of doing
the computations, called iteration, in which there are no operations waiting
for procedure calls to return values, and hence no return table need be con-
structed. The factorial procedure and Fibonacci sequences are introduced. To
compare the efficiency of various methods for computing them, we investigate
the growth of execution time as the argument grows, demonstrating linear
and exponential growth rates.
We begin with three more examples of recursive procedures, with the recursion
being done over the top-level items in lists. In our examples of recursion
involving lists, we made the recursive step by applying the procedure to the
cdr of the list. The car of the list was then treated as a unit, which is why the
recursion was over the top-level items in the list. We refer to a recursion over
the top-level items of a list as aflat recursion, and we say that the procedure
so defined is flatly recursive or simply a flat procedure.
We define append using recursion on the first list. Isl. Cdring on Isl ulti-
mately produces the base case in which Isl is empty. In the base case, when
Isl is empty. Is2 is returned. Thus we can begin the definition with the base
case:
(define append
(laabda (Isl ls2)
(if (null? Isl)
ls2
... )))
To express (append Isl ls2) in terms of (append (cdr Isl) ls2). observe
that (append (cdr Isl) ls2) differs from (append Isl ls2) only in the
absence of the first top>-level item in Isl. For example, if Isl is (a b c) and
ls2 is (d e), then (append (cdr Isl) ls2) gives us (b c d e). and only
(car Isl) remains to be included. Thus when Isl is not empty, (append
Isl ls2) is the same as (cons (car Isl) (append (cdr Isl) ls2)). We
can therefore complete the definition of append.
(define append
(lambda (Isl ls2)
(if (null? Isl)
Is2
(cons 'car la 1)
( (append (cdr Issi) l82)))))
(reverse '
(1 2 3 4 5) ) =* (5 4 3 2 1)
(define reverse
(lambda (Is)
(if (null? Is)
'()
... )))
To get (reverse Is) from (reverse (cdr Is)), we must append the list
that is the value of (reverse (cdr Is)) to the front of the list that is the
value of (list (car Is)). We then complete the definition with
(define reverse
(leunbda (Is)
(if (null? Is)
'()
For example, (2.3 4.7 5 8.1) is sorted in increasing order. If we have two
lists, each sorted in increasing order, we can merge them into a single list in
increasing order. For example, if the list given above is merged with the list
(1.7 4.7), we get the list (1.7 2.3 4.7 4.7 5 8.1).
Let us now write a procedure merge, which takes two n-tuples, sorted-
ntpll and sorted-ntpl2, which have already been sorted in increasing order.
and builds the list obtained by merging them into one sorted n-tuple. If either
list is empty, merge returns the other list. Otherwise we compare the car of
the lists and cons the smaller one onto the list obtained by merging the rest
(define merge
(lambda (sorted-ntpll sorted-ntpl2)
(cond
((null? sorted-ntpll) sorted-ntpl2)
((null? sorted-ntpl2) sorted-ntpll)
((< (car sorted-ntpll) (car sorted-ntpl2))
(cons (car sorted-ntpll)
(merge (cdr sorted-ntpll) sorted-ntpl2))
(else (cons (car sorted-ntpl2)
(merge sorted-ntpll (cdr sorted-ntpl2) )) ) ))
The definition of reverse used the procedure append, which was defined
earlier. It does not matter which was defined first, as long as both are defined
when the procedure reverse is invoked.
The test of whether a nonnegative integer is even or odd gives us another
good example of one procedure using another in its definition. There are many
more direct ways of defining the predicates even? and odd?, but the one we
present now was chosen because it illustrates how each of two procedures
invokes the other in its definition. We use the fact that an integer is even if
its predecessor is odd and odd if its predecessor is even. Starting with any
nonnegative integer, reducing it successively by 1 will eventually bring it to
0. which is even. This analysis leads us to the following definitions:
(define even?
(lambda (int)
(if (zero'? int)
ft
(odd? (subl int ) ) ) )
(define odd?
(lanbda (int)
(if (zero? int)
«f
(even? (subl int)))))
In the definition of the procedure even?, the procedure odd? is called, and in
top-level occurrences of item have been removed from Is. The base condition
is (null? Is), and when it is true, the empty list is returned. Thus we begin
our definition with:
(define remove
(leunbda (iten Is)
(cond
((null? Is) '())
... )))
Next, if Is is not empty, (remove item (cdr Is))is exactly the same as
(remove item Is) when the first item in Is is item, for that item is removed.
On the other hand, when the first item in Is is not item, then we must cons
it onto (remove item (cdr Is)) in order to get (remove item Is). Thus
(define reaove
(lambda (it en Is
(cond
((null? Is) '())
((equal? (car Is) item) (remove item (cdr Is)))
(else (cons (ceir Is) (remove item (cdr Is)))))))
a version named remv that uses eqv? to test for sameness. The exercises
contain other procedures involving flat recursion that go to the end of the
lists instead of stopping after the first occurrence of a given item.
Exercises
obtained by replacing each top-level occurrence of the item old in the list Is
by the item new. Test your procedure on:
In this section, we consider recursion over all the sublists of a list. We say
that the sublist (b c) is nested in the list (a (b c)). It is convenient to have
some way of describing how deep the nesting is. If an item is not enclosed by
parentheses, that item has nesting level 0. For example, the item bird has
nesting level 0. The elements of a list such as (a b c) have nesting level 1.
Thus b has nesting level 1 while the whole list (a b c) has nesting level 0.
Then each additional layer of parentheses adds 1 to the nesting level, so that
the nesting level of the item c in (a (b (c d))) is 3. The objects in the list
that have nesting level 1 are the top-level objects of the list. The top-level
objects in the list (a (b c) (d (e f ))) are a, (b c), and (d (e f )).
We define a procedure count-all with parameter Is that counts those
items in the list Is that are not pairs. For example
that are included as items within the lists of Examples 1,2, and 3 are counted
as atomic items in the lists.
(define count-all
(lambda (Is)
(cond
((null? Is) 0)
... )))
If Is is not empty, we proceed eis we did in our previous examples and consider
how we can get (count-all Is) from (count-all (cdr Is)). The two
diff'er by the number of atomic items in (ceu: Is). If (car Is) is atomic,
then (count-all Is) has a value that is just one greater than the value of
(count-all (cdr Is)). Thus we can continue the definition with:
(define count-all
(lambda (Is)
(cond
((null? Is) 0)
((not (pair? (car Is))) (addl (count-all (cdr Is))))
... )))
When (car Is) is a pair (as is the case in Examples 1 and 3), we must count
the atomic items in (.cax Is) and add that amount to the value of (count-
all (cdr Is)) to get the value of (count-all Is). Thus we complete the
definition with:
(define count-all
(lambda (Is)
(cond
((null? Is) 0)
((not (pair? (car Is))) (addl (count-all (cdr Is))))
(else (+ (count-all (car Is)) (count-all (cdr Is)))))))
In fact, we can combine the last two cond clauses if we write the definition as
follows:
The recursion described differs from flat recursion in that when the car of the
list is a pair, we apply the procedure being defined both to the car and to the
cdr of the list. In flat recursion, the procedure being defined was applied only
to the cdr of the list. When the recursion is over all of the atomic items of a
list, so that in the recursive step the procedure is applied to the car of the
list and to the cdr of the list, we call it a deep recursion. A procedure defined
using a deep recursion will be referred to as a deeply recursive procedure or
simply a deep procedure to distinguish it from a flat procedure. Deep recursion
is also called tree recursion.
Before leaving the definition of count-all, we should observe that we could
have avoided the use of the not in the second cond clause by changing the
order in which we considered the last two cases. That would give us the
definition:
(define count-all
(lambda (Is)
(cond
((null? Is) 0)
((pair? (car Is))
(+ (count-all (car Is)) (count-all (cdr Is))))
(else (+ 1 (count-all (cdr Is)))))))
Many of the flat procedures defined earlier have analogs that are deep pro-
cedures. To we consider the procedure remove-all, which is
illustrate this,
item Is) is the same as (remove-all item (cdr Is)), and we have:
(define remove-all
(Icifflbda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (remove-all item (cdr Is)))
... )))
If (cax Is) is a pair that is not the same as item, then we remove all occur-
rences of item from (car Is) and cons the result onto (remove-all item
(cdr Is)). Thus,
(define remove-all
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (remove-all item (cdr Is)))
((pair? (ccir Is))
(cons (remove-eill item (ceir Is)) (remove-all item (cdr Is))))
... )))
Finally, if (car Is) is atomic and is not the same as item, we must cons it
back onto (remove-all item (cdr Is)) in order to get (remove-all item
Is). Wewrap up the definition in Program 4.8. We can combine the last two
cond clauses if we rewrite the definition as follows:
(define remove-all
(leimbda (item Is)
(cond
((null? Is) '())
((equal? (c«ur Is) item) (remove-all item (cdr Is)))
(else (cons (if (pair? (ceir Is))
(remove-all item (car Is))
(car Is))
(remove-all item (cdr Is)))))))
(define remove-all
(laj&bda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (remove-all item (cdr Is)))
((pair? (car Is))
(cons (remove-all item (car Is)) (remove-all item (cdr Is))))
(else (cons (car Is) (remove-all item (cdr Is)))))))
we again see that when (car Is) is a pair not equal to item,
In this example,
the procedure remove-all is applied recursively to both the car and the
cdr of Is. Thus remove-all displays this characteristic behavior of deeply
recursive procedures.
(define remq-all
(leifflbda (symbl Isi)
(cond
((null? Is) •())
((pair? (car Is))
(cons (remq--all symbl (car Is)) (remq-all symbl (cdr Is.))))
((eq? (car Ifi) symbl) (remq -all symbl (cdr Is)))
(else (cons [car Is) (remq-.all s ymbl (cdr Is)))))))
When the flat procedure reverse is applied to a list, we get a new list with
the top-level objects in reverse order. Thus,
We can also define a procedure reverse-all that not only reverses the order
For the base case, the list is empty, and (reverse-all '()) returns the
empty list. Thus the definition begins with:
(define reverse-zdl
(lambda (Is)
(cond
((null? Is) '())
... )))
(define reverse-all
(lambda (Is)
(cond
((null? Is) '())
((pair? (ceur Is))
(append (reverse-cill (cdr Is))
(list (reverse-all (cau: Is)))))
... )))
In the remaining case, (car Is) is not a pair, so we merely place it at the
(define reverse-all
(lambda (Is)
(cond
((null? Is) '())
((pair? (car Is))
(append (reverse-all (cdr Is))
(list (reverse-all (car Is)))))
(else
(append (reverse-adl (cdr Is))
(list (car Is)))))))
of reverse-all:
(define reverse-all
(lambda (Is)
(if (null? Is)
'0
(append (reverse- all (cdr Is))
(list (if (pair? (car Is))
(reverse-all (car Is))
(car Is)))))))
Exercises
a node that is located at the root. Each top-level object in the list forms a
new node connected to the root node by a branch. Each sublist itself then
becomes the root of a subtree, and the tree grows downward. For example,
the tree representing the list (a (b c d) ((e f) g) ) is given in Figure 4.11.
Each item or sublist of the original list is a node of this tree. Each sublist is
itself the root of a subtree of the original tree. Thus ((e f) g) corresponds
to the subtree given in Figure 4.12.
An item at the lower end of a branch that is not the top end of another
branch is called a leaf of the tree. We can readily see how deeply an item
is nested in the list by looking at its nesting level in the tree. For example,
in Figure 4.11, the leaf a is at nesting level 1 and the leaf e at nesting level
3. We say that the depth of a list is the maximum of the nesting levels of all
of its items. The list (a (b c d) ((e f) g)) has depth 3. With the tree
growing downward, we can say that the depth of a list is the nesting level of
its lowest leaves.
To traverse a tree, that is, to move down the tree from one node to another,
we use the procedures cau: and cdr. Taking the car of a list corresponds to
((e f) g)
((e f) g)
moving down one node on the leftmost branch of the tree. Taking the cdr
of a list corresponds to considering the tree that when the leftmost
is left
branch is omitted. Thus when taking the car, we move down one level on
the tree. When taking the cdr, we stay at the same level of the tree. With
an appropriate sequence of car and cdr applications, we can reach any node
of a tree. For example, in the tree in Figure 4.11, the node (e f) is reached
using caaddr.
We define a procedure depth that takes item as its argument and returns
its depth. The item may be either atomic or a list. If item is atomic, we
(define depth
(lambda (item)
(if (not (pair? item))
assign it depth 0. Since the empty list is atomic, it also has depth 0. We take
as the base case for the recursive definition the test (not (pair? item)), for
that corresponds to being at a leaf of the tree. We begin the definition of
depth with:
(define depth
(lambda (item)
(if (not (pair? item))
... )))
The depth of the whole tree is the larger of the depth of its leftmost branch
and the depth of the rest of its branches. Taking the cair of the list moves
down one node on the leftmost branch, so that the depth of the whole leftmost
branch is one greater than the depth of (car item). The depth of the rest
of the branches is just the depth of (cdr item). This gives us the definition
displayed in Program 4.13.
The procedure depth gives us the maximum number of levels in a tree
representing its argument. We next define a procedure that gives us a list of
the leaves on the tree as a list of atomic items, where each leaf is raised out
of its sublist to be at top level. We call this procedure flatten. When we
apply it to the ((e f ) g)), we get (a b c d e f g). The
list (a (b c d)
parameter of the procedure flatten will be Is. The base case is the empty
list, which flattens into itself. Thus we begin the definition of flatten with:
(define flatten
(leunbda (Is)
(cond
((null? Is) '())
... )))
When Is is not empty, we build (flatten Is) from (flatten (cdr Is))
by first determining whether (car Is) is a pair. If it is, we flatten (car Is)
(define flatten
(lEunbda (Is)
(cond
((null? Is) '())
((pair? (car Is))
(append (flatten (car Is)) (flatten (cdr Is))))
... )))
(define flatten
(lambda (Is)
(cond
((null? Is) '())
((pair? (car Is))
(append (flatten (car Is)) (flatt<an (cdr Is))))
(else (cons (car ]-s) (flatt<sn (cdr Is)))))))
We have discussed flat and deep recursion. A flat recursion is over the
top-level items of a list. This is equivalent to a recursion over the nodes of
the corresponding tree, which are one level below the root. A deep recursion
is over all of the items in the list. This is equivalent to a recursion over the
leaves of the corresponding tree. That is why deep recursion is also referred
to as tree recursion.
We conclude this section with an example of a procedure that removes an
item from a list but only the first (leftmost) occurrence of that item in the
list. Let us name the procedure remove-leftmost and look at a couple of
examples.
is empty, the empty list is returned. Thus we begin the definition with the
terminating condition:
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
... )))
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
... )))
If (car Is) is atomic and is not the same cis item, the answer is obtained
by consing (car Is) to the list obtained by removing the leftmost item from
(cdr Is). Thus we get:
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
((not (pair? Is))
(cons (car Is) (remove-leftmost item (cdr Is))))
... )))
We still have the case in which (car Is) is a nonempty list not equal to
item. If we analyze the recursion by looking at
we see that we get a list with the first occurrence of item removed; but we
do not know whether this Weis the first occurrence of item in Is. We want to
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
((not (pair? (car Is)))
(cons (car Is) (remove--leftmost item (cdr Is))))
( (member-all? item (car Is))
(cons (remove-leftmost item (car Is)) (cdr Is)))
(else (cons (car Is) (remove--leftmost item (cdr Is.)))))))
(define member-all?
(lambda (item Is)
(if (null? Is)
#f
(or (equal? (ceo- Is) item'
(and (not (pair? (car Is)))
(member-all'' item (cdr Is)))
(and (pair? (car Is))
(or (member--all? item (car Ie.))
(member--all? item (cdr Is.))))))))
remove only the first occurrence of item in Is, and its first occurrence may
not be in (cdr Is). In order to use this kind of argument, we must first
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
((and (pair? (car Is)) (member-all? item (car Is)))
(cons (remove-leftmost item (car Is)) (cdr Is)))
(else (cons (car Is) (remove--leftmost item (cdr Is)))))))
The recursion in the procedure remove-leftmost differs from the list re-
cursions done earlier in that we have to test whether item is in the car of the
list before proceeding to build the answer. This means cdring through the
car of the list twice in some cases. We shall return to the consideration of
remove-leftmost in Chapter 5, where a definition is presented that avoids
this double cdring. We have now seen various examples of both flat and deep
(tree) recursions.
Exercises
(count-parens-all '()) ^^ 2
(count -parens -all '((a b) c)) ^^ 4
(count-parens-all '(((a () b) c) () ((d) e))) =» 14
(define fact
(lambda (n)
(if (zero? n)
1
the number multiplied successively by all the positive integers less than that
number. For example, (fact 5) has the value 5x4x3x2x1 = 120. We
derive this procedure using much the same kind
we used with
of reasoning as
lists, but instead of using cdr to reduce the size of the argument, we use subl.
1. The recursive step in this case is done by considering (fact (subl n)),
which gives us the successive products of all of the positive integers less than
n. To get (fact n) from (fact (subl n)), all we have to do is multiply it
where answer-2 is (fact 2). Thus the first two rows of the return table are:
answer-1 is (* 3 answer-2)
answer-2 is (fact 2)
answer-1 is (* 3 answer-2)
answer-2 is (* 2 answer-3)
answer-3 is (fact 1)
answer-1 is (* 3 answer-2)
answer-2 is (* 2 answer-3)
answer-3 is (* 1 answer-^)
answer-4 is (fact 0)
where (fact 0) is 1. Now that we have found that answer-4 is 1, we work our
way up the table, replacing each answer on the right side by the value obtained
for it in the row below. This process is known as backward substitution. This
gives us:
answer-4 " 1
answer-3 is 1
answer- 2 is 2
answer- 1 is 6
so (fact 3) is 6. In finding (fact 3), the return table has four rows. In the
last row, the value of the variable on the left was obtained directly from the
terminating condition of the program. Then each of the other three variables
on the right was computed with a multiplication, so there were three multi-
plications required to complete the computation of (fact 3). The building
up of the return table and the subsequent backward substitution may be
summarized in the following:
(* 3 (fact 1)))
(* 2 :
(* 3 (* 1 (fact 0))))
(* 2
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 D)
(* 3 2)
6
invocations of procedure fact. Thus the return table has n+ 1 rows. In the
last row, the value on the right is found to be 1 — the value returned when the
terminating condition is true. In each of the other n rows of the return table,
a multiplication is performed to find the value on the right, making a total of
n multiplications to complete the computation.
We have seen that in programs like the one written for fact, there is an
operation waiting for the value returned by the recursive procedure call. The
computational process so defined is not implemented as an iterative process.
On the other hand, we saw several iterative procedures, such as member?, in
which no operations waited for values returned by the recursive procedure
calls. some programming language implementations, when an iterative
In
procedure is executed, it is still possible that a return table is built up and later
reduced by backward substitution. However, in Scheme, when a procedure is
intended to be iterative, the computation is always implemented in such a
way that no return table is needed.
illustrated in the following table. The initial values of n and aee are in the
first row. The entries in the first column decrease by 1 while each entry in the
second column is computed by multiplying the two entries in the preceding
row.
n aee
3 1
2 3
1 6
6
To define fact-it, we begin with the base case for which n is zero. When
(zero? n) is true, the accumulator has the answer, so aee is returned. Thus
we begin the definition with:
(define fact-it
(lanbda (n ace)
(if (zero? n)
ace
... )))
If n is not zero, we call faet-it with n reduced by one and the accumulator
multiplied by n, so the definition is completed with:
turned:
of fact-it is seen when we look at the last line of the definition. After the
procedure call, there is no further operation to be done. Compare this last
line with the last line,
in the definition of fact. We see that after the procedure fact is called, the
result must still be multiplied by n. When fact-it is called, no additional
operations are performed on the result. Thus fact-it runs as an iterative
process, but fact does not. When we trace this iterative procedure, we see
that the computation does not build up a return table of operations waiting
for values to be returned.
If we count the number of times we call the procedure fact-it and the
more dramatic.
To compute the we invoke (fact -it 3 1). If we do not like
factorial of 3,
to write the extra argument for the accumulator, we can define an iterative
version of fact that takes only one argument by writing
(define fact
(lambda (n)
(fact-it n 1)))
Exercise 4-i2
Enter the procedure fact into the computer and compute (fact n) for n =
10, 20, 30, 40, 50 and 100. You will have an opportunity to observe how
the implementation of Scheme you are using displays large numbers.
Exercise 4-i3
What happens when you invoke (fact 3,5)?
, 1111
Test your procedure by summing the harmonic series for 10 terms, 100 terms,
1000 terms, and 10,000 terms. It can be shown that
11 1 , ,11 1
23 n - ° - 23 n-1
where logn is the natural logarithm of n. Using the Scheme procedure log,
verify this inequality for the values of the sums computed above.
offsprings, so we have three pairs. At the end of three months, the first pair
of offsprings is old enough to bear young, so this time we get two new pairs,
the sequence given above. Observe that each number in the sequence is the
sum of the two numbers preceding it. It has become customary to begin the
sequence with 0, 1, and use the algorithm that says that the next number
is always the sum of the preceding two numbers. The nth number in this
sequence is called the nth Fibonacci number.
We now define a procedure fib that takes a nonnegative integer n as its
n > 1, (fib n) is the sum of (fib (- n 1)) and (fib (- n 2)). We now
use this last recursive condition to define the procedure fib in Program 4.20.
(define fib
(lambda (n)
(if « n 2)
n
(+ (fib (- n D) (fib (- n 2))))))
(fib 4)
(fib 3) (fib 2)
(fib 1) (fib 0)
From Figure 4.21, we see that each node corresponds to a procedure call that
is made in evaluating (lib 4). In this case, there are nine procedure calls.
Each branch point (a node from which two branches originate) corresponds
we can build a
to an addition, so there are four additions. In a similar way,
recursion tree for (lib 5), and we will have fifteen nodes and seven branch
points, hence fifteen procedure calls and seven additions. We suggest that you
draw the recursion trees for (lib 5) and for (lib 6) to see how large they
are and count the number of procedure calls and additions. It is not difficult
to see from the trees that if (calls-lib n) tells how many procedure calls
there are in computing (lib n) and (adds -lib n) tells how many additions
there are in computing (lib n), then these procedures satisfy the relations
(calls-lib 0) la 1
(calls-fib 1) is 1
and
(adds-f ib 0) is
(adds-fib 1) ts
n
(fib n)
012345678
1 1 2 3 8 13 21
5 34 55
9 10
inefficiency since the same fib is called with the same arguments a number
of times, so that the different recursive calls repeat each other's work. In the
tree shown in Figure 4.21, (fib 2) is invoked twice and (fib 1) is invoked
three times. We next look at an iterative method for computing the Fibonacci
numbers.
A clue to how to set up an iterative process for computing the Fibonacci
numbers is found by observing that it takes the previous two numbers to
compute the next number in the sequence. Thus we have to store two jiumbers
at each step. We begin by storing the first two Fibonacci numbers, and 1
accl acc2
1
At each step, accl holds the current Fibonacci number and acc2 holds the
next one. Thus we can describe the algorithm that takes us from one step to
the next as follows:
1. The new value of accl is the same as the previous value of acc2.
2. The new value for acc2 is the sum of previous values of accl and acc2.
We apply these rules to extend the table to show the next six steps, as dis-
turns the Fibonacci number corresponding to n. There are two ways that we
can use the algorithm given to write the code. In the first method, we can
use the value stored in accl (initially 0) to give us the answer. In that case,
one iteration of the algorithm gives us (fib 1), two iterations give us (fib
2), and in general n iterations give us (fib n) for any positive n. In the
(define fib-it
(lambda (n accl acc2)
(if (= n 1)
acc2
(fib-it (subl n) acc2 (+ accl acc2)))))
second method, we can use the value stored in acc2 (initially l) to give us
the answer. In this case, one iteration of the algorithm gives us (fib 2), two
iterations give us (fib 3), and in general, (n— 1) iterations give us (fib n).
The second method is more efficient for getting the value of (fib n). We opt
to implement the second method.
Our iterative procedure fib-it takes three parameters: the positive integer
n and the two accumulators accl and acc2. To implement the algorithm
stated above, we successively replace acc2 by the sum of accl and acc2,
and replace accl by the previous value of acc2. Then to compute the nth
Fibonacci number, we must repeat the process (n — 1) times. We use the
variable n as a counter and reduce it by one on each pass. When n reaches 1,
the accumulator acc2 contains the answer. This leads to the definition given
in Program 4.24.
(fib-it 6 1)
(fib-it 5 1 1)
(fib-it 4 1 2)
(fib-it 3 2 3)
(fib-it 2 3 5)
(fib-it 1 5 8)
8
and the answer is the final value of acc2, which is 8. To compute the sixth
Fibonacci number, we only make six procedure calls and 5 additions. In
general, to compute the nth Fibonacci number, we make n procedure calls
and do n — 1 additions. This is a noticeable improvement over the number
of procedure calls and additions when fib is invoked. The iterative version,
fib- it, is certainly more efficient and saves a considerable amount of time
in computing the Fibonacci numbers. The ordinary recursive version, fib, is
less efficient but it does have the advantage of being easier to define directly
in terms of the rule that defines the Fibonacci numbers.
(define fib
(lambda (n)
(if (zero? n)
(fib-it n 1))))
We have seen that some methods of evaluating a given expression may take
more resources than other methods. The study of the efficiency of various
algorithms is called the analysis of algorithms. Let us denote the total re-
sources used in computing an expression that depends on an argument n to
be (res n). In our discussion, fib depended on the argument n, and we can
define as the resources used the sum of (calls-fib n) and (adds-^fib n).
Inspection of the table for (calls-fib n) shows that the following relation
exists between (calls-fib n) and (fib n):
so that
We now derive an estimate for (fib n). If you prefer, you can skip to
the formula for (fib n) given at the end of the derivation. We use the
fact that if a procedure satisfies the Fibonacci recurrence relation F{n) =
F{n — 1) + F{n - 2) and the initial conditions F(0) = and F{1) = 1, then
F[n) = (fib n) for all n. We begin by making a rather arbitrary assumption:
that F[n) gets large like some number a raised to the nth power. We then look
for restrictions that can be placed on the number a in order for the function
a" to satisfy the Fibonacci recurrence relation. If we are lucky enough to find
such conditions that determine a, we have solved the problem of finding a
formula for F{n). Substitution of a" into the recurrence relation gives us
„n— 1
^n
a == a + a„n—2
I
a^ =0 + 1
—
(l + v/5)
a
/'(0) = =A+5
F(l) = l = .4a + 56
We find that A — -B = l/\/E and that with these values of A and B, F{n)
and (lib n) are the same for n = and n = 1, and that they both satisfy
the Fibonacci recurrence relation for all n. This means that they are the same
for £dl n, and we have
Thus (lib n) is somewhat less than 1.7", and (res n) is somewhat less than
3(1.7").
In general, we say that the procedure (res n) is of order 0{f{n)) for some
function / of n if there is a constant K such that (res n) < Kf{n) when n
is sufficiently large. we can say (res n) = 0(1.7") and since it
In our case,
grows like the nth power of a number greater than 1, we say that (res n)
has exponential order when computing (lib n)
On the other hand, the operation count (res n) for computing (lib-it
n 1) is 2n - 1, which is simply 0{n). Here the n does not appear in an
exponent, but rather (res n) is simply a constant times n. We say that in this
case, (res n) has linear order. Thus the time required to compute (lib n)
grows exponentially with n, while the time required to compute (lib- it n
(define reverse-it
(lambda (Is ace)
(if (null? Is)
ace
(reverse-it (cdr Is) (cons (cEir Is) ace)))))
(define reverse
(lambda (Is)
(reverse-it Is '())))
empty, whereas in the recursive version, we still have to use backward substi-
tution in a return table to get the answer. Furthermore the iterative version
does not use the helping procedure append. Generally, iterative versions tend
to require more arguments.
Exercises
Exercise 4-15
Rewrite the recursive version of the procedure fib with the line
inserted just below the line (Isimbda (n). Then compute (fib 4) and com-
pare the results with the tree in Figure 4.21. Also compute (fib 5) and (fib
6) and observe how the number of recursive calls to fib increases.
(writeln "n = " n ", accl = " accl ", acc2 = " acc2)
Compute (fib-it 4 and compare the output with the output for (fib
1)
6 1).
this section. Test your procedures on the values given in Table 4.22. Also
evaluate each of these procedures for larger values of n to get an idea of their
rates of growth.
(a b a c a d)
(b c a (b a) c a)
(b (c d))
5.1 Overview
When we bind a variable to some value using define, we are able to use that
variable to represent the value to which bound either directly in response
it is
You may have wondered how Scheme knows what value to associate with
various occurrences of a variable. When some value is assigned to a variable,
we may think of that information being stored in a table with two columns: the
left one for variable names and the right one for the associated values. Such
kept in a table which we call the initial global environment. This initial global
environment is in place whenever you start up Scheme. When a given variable
is encountered in an expression, Scheme looks through its environment to see
if bound to a value. Naturally, the variable + is bound
the variable has been
to the arithmetic operation we usually associate with the addition procedure,
and so on.
In addition to having the predefined Scheme variables, we have seen how
to use define to bind a variable to a desired value. The expression (define
var val) binds the variable var to the value val. We can again think of the
variables we define ourselves as being placed in a table which we call the user
global environment and when a variable is encountered in an expression, the
global environment (which includes both the user and initial global environ-
ments) is scanned to see if that variable is bound to a value. If a binding
cannot be found, a message is written saying that the variable is unbound in
the current environment. The user global environment remains in effect until
the user exits from Scheme.
Variables are also used as parameters to procedures that are defined by a
lambda expression. For example, in the lambda expression
(lambda (x y) (+ x y))
(danbda (x y) (+ x y)) 2 3)
we can think of a new table being made, called a local environment, which
is associated with this procedure call. In this local environment, x is locally
bound to 2 and y is bound
locally to 3. Then substituting 2 for x and 3 for
((lambda (x y) (+ x y)) 2 3)
(lambda (f y) (f a (f y z)))
the variables f and y are lambda bound in the expression, and the variables
a and z are free in the expression. When the application
is evaluated, the operator (which is the lambda expression) and its two oper-
ands are first evaluated. When the lambda expression is evaluated, bindings
are found for the free variables in a nonlocal environment. Then, with these
bindings for the free variables, the body of the lambda expression is evaluated
with 1 bound to the procedure, which is the value of cons, and y bound to
3. If either of the free variables is not bound in a nonlocal environment, a
message to that effect appears when the application is made. On the other
hand, if a is bound to 1 and z is bound to (4) in a nonlocal environment,
then this application evaluates to (1 3 4).
We used the term nonlocal environment in the previous paragraph when
we referred to the bindings of the free variables in the body of a lambda
expression. Those bindings may be found in the global environment or in a
local environment for another lambda expression. This is illustrated by the
following example:
((lambda (x)
((lambda (y)
(- X y))
15))
20)
The variable x is free in the body of the inner lambda expression, but its
binding is found in the local environment for the outer lambda expression.
The value of the expression is 5.
In the example
(lambda (x y) (+ x y))
the local bindings hold only in the body (+ x y) of the lambda expression,
and when we leave the body, we can for the moment think of the local en-
vironment as being discarded. The expression (+ x y) is said to be in the
y). In general, an expression is said to
scope of the variable x (and also of
be in the scope of a variable x if that expression is in the body of a lambda
expression in which x occurs in the list of parameters.
By looking at a Scheme program, one can tell whether a given expression is
in the body of some lambda expression and determine whether the variables
in that expression are lambda bound. A language in which the scope of the
variables can be determined by looking only at the programs is called lexically
scoped. Scheme is such a language.
in the expression body, we use a let expression (which is a special form with
To make several such local bindings in the expression body, say vari is to
The scope of each of the variables vari, var2, . , vaVn is only body within
the let expression. For example, the expression
the variable add2 is bound to the procedure to which (lambda (x) (+ x 2))
evaluates, which increases its argument by 2, and b is bound to 0.5, and the
whole expression returns 0.2.
The local binding always takes precedence over the global or other nonlocal
bindings, as illustrated by the following sample computation:
((lambda (a b) (+ a b)) 2 3)
((lambda ivari var2 ... varn) body) vali va/2 ... vain)
From this representation, we see that any free variable appearing in the
operands vali, va/2, . ., vain is looked up in a nonlocal environment. For
example, let's consider
( a b)) 30
30
found there. We
must then look in the nonlocal (here global) environment,
and there we find that b is bound to 2. With these values, (* a b) is 30, so
the let expression has the value 30. In [4] we see a similar program in which
,
the free variables are looked up in a nonlocal but not global environment.
Looking back at the let expressions, we see how the lexical scoping helps us
decide which environment (local or nonlocal) to use to look up each variable.
It is important to keep track of which environment to use in evaluating an
expression, for if we do not do so, we might be surprised by the results. Here
is an interesting example:
Because b is bound to 10 in [2] and (addb 25) is the body of the let expres-
sion with this local environment, one might be tempted to say that the answer
in [2] should have been 35 instead of 125. In [1] , however, the lambda ex-
pression falls within the scope of the let expression in which b is bound to
^ The symbol +is also free in (+ a 5 ) and its value is found in the initi£d global environment
,
to be the addition operator. The number 5 eveduates to itself. Simileirly, the symbol » is free
in the body, eind its value is found in the initial globed environment to be the multiplication
operator.
applied: (1) the list of parameters, which is (x), (2) the body of the lambda
expression, which is (+ x b), and (3) the nonlocal environment in which the
free variable b is bound, which is the environment created by the let expression
in which b is bound to 100. The value of a lambda expression is a procedure
(also called a closure), which consists of the three parts just described. In
general, the value of any lambda expression is a procedure (or closure) that
consists of (1) the list of parameters (which follows the keyword lambda), (2)
the body of the lambda expression, and (3) the environment in which the
free variables in thebody are bound at the time the lambda expression is
evaluated. When the procedure is applied, its parameters are bound to its
arguments, and the body is evaluated, with the free variables looked up in
the environment stored in the closure. Thus in [2], (addb 25) produces the
value 125 because the addb is bound to the procedure in which b is bound to
100.
Consider the following nested let expressions:
The first let expression sets up a local environment that we call Environment 1
(Figure 5.1).
m
Figure 5.1 Environment 1
0.5
All of the variables in the expression to which add2 is bound are either
bound in that expression itself (a^ was x) or are bound outside of the let
expression (as was b). We are now ready to evaluate the expression (/ b
(add2 b)). In which environment do we look up b? We always search the
environments from the innermost let or lambda expression's environment out-
ward, so we search Environment 2 first, finding that b is bound to 0.5. Thus
the whole expression is (/ 0.5 2.5), which evaluates to 0.2.
As an example of how let is used in the definitions of procedures, we
reconsider the definition of the procedure remove-leftmost, which was given
in Program 4.15. Recall that our objective is to produce a list the same
as the list Is except that it has removed from it the leftmost occurrence
of item. In the base case, when Is is empty, the answer is the empty list.
If (car Is) is equal to item, (car Is) is the leftmost occurrence of item
and the answer is (cdr Is). If neither of the cases is true, there are two
possibilities: either (car Is) is a pair, or it is not a pair. If it is a pair,
we want to determine whether it contains item. In Program 4.15, we used
member-all? to determine this. Another way is to check whether (car Is)
changes when we remove the leftmost occurrence of item from it. If so, then
item must belong to (car Is), in which case the answer is
twice, once when making the test and again when doing the consing. To avoid
the repeated evaluations of the same thing, we use a let expression to bind a
variable, say r em-list, to the value of
and use rem-list each time the value of this expression is needed. Here is
(define remove-leftmost
(lambda (item Is)
(cond
((null? Is) '())
((equal? (car Is) item) (cdr Is))
((pair? (car Is))
(let ((rem-list (remove-leftmost item (car Is))))
(cons rem-list (cond
((equal? (ceir Is) rem-list)
(remove--leftmost item (cdr Is )))
(else (cdr Is))))))
(else (cons (ecu: '.
In a let expression
any variables that occur in val and are not bound in the expression val itself
must be bound outside the let expression (i.e., in a nonlocal environment), for
in evaluating val, Scheme looks outside the let expression to find the bindings
will return a message that fact is unbound. You should try entering this code
to become familiar with the messages that your system returns. This message
refers to the fact occurring in the lambda expression (written here in italics),
but now any of the variables vari, var2, • • • , varn can appear in any of the
expressions vali , va/2 , • • • , vain , and refer to the locally defined variables
Ifwe call (fact 0), the value 1 is returned, since the consequent of the if expression is
true and the alternative, in which the call to fact is made, is not evaluated. In this case
no error message would result.
(define fact
(lambda (n)
(letrec ((fact-it
(lambda (k ace)
(if (zero? k)
ace
(fact-it (subl k) (* k ace))))))
(fact-it n 1))))
(define swapper
(lambda (x y Is)
(letrec
((swap
(lambda (Is*)
(eond
((null? Is*) '())
((equal? (car Is*) x) (eons y (swap (cdr Is*))))
((equal? (ceir Is*) y) (eons X (swap (cdr Is*))))
(else (cons (<:ar Is*) (swap (cdr Isi'))))))))
(swap Is))))
procedures that take several arguments, many of which stay the same through-
out the program. For example, consider the procedure swapper defined in
Program 2.8, which has three parameters, x, y, and Is, where x and y are
items and Is is a list. Then (swapper x y Is) produces a new list in which
x's and y's are interchanged. Note that in Program 2.8 each time we invoked
swapper recursively, we had to rewrite the variables x and y. We can avoid
this rewriting if we use letrec to define a local procedure, say swap, which
takes only one formal argument, say Is*, and rewrite the definition of the
procedure swapper as shown in Program 5.5.
swap is called in the leist line of the code, its argument is Is, which is lambda
bound in the outer lambda expression. We could just as well use the variable
Is instead of Is* as the parameter in swap since the lexical scoping specifies
which binding is in effect. When we
swapper recursively in the old code,
call
we write all three arguments, whereas when we call swap recursively in the
new code, we must write only one argument. This makes the writing of the
program more convenient and may make the code itself more readable.
In this section, we have seen how to bind variables locally to procedures
using the special forms with keywords let and letrec. We use these impor-
tant tools extensively in writing programs that are more efficient and easier
to understand.
Exercises
Exercise 5.1
Find the value of each of the following expressions, writing the local environ-
ments for each of the nested let expressions. Draw arrows from each variable
to the parameter to which it is bound in a lambda or let expression. Also
draw an arrow from the parameter to the value to which it is bound.
a. (let ((a 5))
(let ((fun (lambda (i) (max x a))))
(let ((a 10)
(x 20))
(fun 1))))
Exercise 5.2
Find the value of each of the following letrec expressions:
a. (letrec
((loop
(lambda (n k)
(cond
((zero? k) n)
(« n k) (loop k n))
(else (loop k (remainder n k) ))))))
(loop 9 12))
Exercise 5.3
Write the two expressions in Parts a and b of Exercise 5.1 as nested lambda
expressions without using any let expressions.
Exercise 5.4
Find the value of the following letrec expression.
(letrec ((mystery
(lambda (tuple odds evens)
(if (null? tuple)
(append odds evens)
(let ((next-int (car tuple)))
(if (odd? next-int)
(mystery (cdr tuple)
(cons next-int odds) evens)
(mystery (cdr tuple)
odds (cons next-int evens))))))))
(mystery '
(3 16 4 7 9 12 24) ' '
()))
Exercise 5.5
We define a procedure mystery as follows:
(define mystery
(leUDbda (n)
(letrec
( (mystery-helper
(lambda (n s)
(cond
((zero? n) (list s))
(else
(append
(mystery-helper (subl n) (cons s))
(mystery-helper (subl n) (cons 1 s) )))))))
(mystery-helper n '()))))
the degree. In general, a term is an expression of the form a^x^ where the ,
coefficient ajk is a real number emd the degree A; is a nonnegative integer. The
expressed in symbols by
c(ajkz*) = (caifc)z*
anX^ + an-ix^~ + • • •
+ a2X + aix + ao
where the coefficients at, for k — 0, . . .,n denote real numbers. The sum of
two polynomials is the polynomial obtained by adding all of the terms of both
polynomials, using the rule given above for adding terms of like degree. Thus
the sum of
is the polynomial
7x^ + 9x'^ + 4x2 + llx-3
The term of highest degree in a polynomial is known as its leading term, and
the coefficient of the leading term is known as its leading coefficient. The
leading term of 3x'* + 5x'^ + 12 is 3x'*, and its leading coefficient is 3.
Our goal is to write programs that produce the sum and product of poly-
nomials. We saw the definition of the sum in the discussion above, and later
we shall define the product. As in our development of exact arithmetic in
Chapter 3, we again assume that certain constructor and selector procedures
for polynomials have been predefined and return to their definition later in
this section when we consider the actual representation of the polynomials in
poly. If poly is a polynomial, then (degree poly) is the degree of poly and
(leading-coef poly) is the leading coefficient of poly. There is a zero poly-
nomial, the-zero-poly, which has degree zero and leading coefficient zero.
Finally, (rest-of-poly poly) is the polynomial obtained from a polynomial
of positive degree, poly, when its leading term is removed. If poly is of degree
zero, (rest-of-poly poly) is the-zero-poly.
The constructor procedure is called poly-cons. If n is a nonnegative inte-
ger, a is a real number, and p is the-zero-poly or a polynomial of degree
(define zero-poly?
(lambda (poly)
(and (zero? (degree poly)) (zero? (leading-coef poly)))))
Program 5.7 shows how we build a term having degree deg and coefficient
coef The term so defined is itself a polynomial of degree deg consisting of
.
(define make-term
(leunbda (deg coef)
(poly-cons deg coef the-zero-poly)))
(define leading-term
(launbda (poly)
(maJce-term (degree poly) (leading-coef poly))))
We next define the procedure p+ such that if polyl and poly2 are two
polynomials, then (p+ polyl poly2) is the sum of polyl and poly2. Let us
recall that the sum of two terms bx'' and ex'' of the same degree fc is a term
(6 + c)x* also of degree k. The sum of two polynomials is then the polynomial
obtained by adding the terms of like degree in the two polynomials. Our
algorithm for adding the two polynomials polyl and poly2 is:
• If the degree of poly2 is greater than the degree of polyl, their sum is a
polynomial that has the same leading term as poly2 and the rest of their
and the leading coefficient of their sum is the sum of the leading coefficients
of polyl and poly2, and the rest of their sum is the sum of (rest-ol-poly
polyl) and (rest-of-poly poly2).
This algorithm for the sum, p+, of polyl and poly2 leads to Program 5.9.
In this program, the use of the let expression enabled us to write each of
the cond clauses more concisely and more clearly. For example, had we not
used the let expression, the first cond clause would have looked like
(define p+
(lambda (polyl poly2)
(cond
((zero-poly? polyl) poly2)
((zero-poly? poly2) polyl)
(else (let ((nl (degree polyl))
(n2 (degree poly2))
(al (leading-coef polyl))
(a2 (leading-coef poly2))
(restl (rest-of-poly polyl))
(rest2 (rest-of-poly poly2)))
(cond
((> nl n2) (poly-cons nl al (p+ restl poly2)))
((< nl n2) (poly-cons n2 a2 (p+ polyl rest2)))
(else
(poly-cons nl (+ al a2) (p+ restl rest2) ) ))) ))))
+ Sx^ +
12x^ 16x3 + 20x2
9x^+6x^ + 12x2 + 15x
Once t* has been defined, the product p* of polyl and poly2 can be defined
using the following algorithm:
• Otherwise, we multiply the leading term of polyl by poly2, and add that
to the product of the rest of polyl and poly2.
This leads us to Program 5.10 for p*. In this program, t* is defined locally
using a letrec expression since it is a recursive definition. The lambda expres-
sion for the procedure p* is placed within the body of the letrec expression for
t* so that the local procedure it defines is available for use in p*-helper. The
letrec expression that defines p*-helper is used because the variable poly2 is
not changed in the recursive invocations of the procedure being defined. Thus
it is programming style not to carry it along as a parameter. We define
better
the local procedure p*-helper that has as its only parameter the polynomial
pi that is bound to polyl when it is later invoked.
Program 5.11 defines a unary operation negative-poly such that when
poly is a polynomial, (negative-poly poly) is its negative: the polynomial
with the signs of all of its coefficients changed. We compute it by multiplying
poly by the polynomial that is a term of degree and leading coefficient —1.
Now we have the negative of a polynomial, we can
that define the difference
p- between polyl and poly2 as shown in Program 5.12.
We now consider how to find the value of a polynomial poly when a number
is substituted for the variable x. To evaluate the polynomial 4x^-|-8z^ — 7aj-|-6
for X = 5, we substitute 5 for x and get 4{5'^) + 8{5^) — 7{b) + 6. The polynomial
(define p*
(letrec
((t* (lambda (trm poly)
(if (zero-poly? poly)
the-zero-poly
(poly-cons
(+ (degree trm) (degree poly))
( (leading-coef trm) (leading-coef poly))
(t* trm (rest-of-poly poly)))))))
(lambda (polyl poly2)
(letrec
((p*-helper (lambda (pi)
(if (zero-poly? pi)
the-zero-poly
(p+ (t* (leading-term pi) poly2)
(p*-helper (rest-of-poly pi)))))))
(p*-helper polyl)))))
(define negative-poly
(lambda (poly)
(let ((poly-negative-one (make-term -1)))
(p* poly-negative-one poly))))
Program 5.12 p-
(define p-
(leunbda (polyl poly2)
(p+ polyl (negative-poly poly2))))
up the terms, and we get a grand total of n{n + l)/2 + n operations. We can
reduce this number of operations significantly by using the method of nested
known as Horner's rule, or synthetic division.
multiplication, also
Before we derive the method of nested multiplication, we consider as an
example the polynomial P{x) = Ax^ + 8x^ - 7x + 6. If we write the constant
term first and factor an x out of the rest of the terms, we get P{x) = 6 +
x(— 7 + 8x + 4x^). We next factor an x out of the terms after the —7 in
the parentheses, to get P{x) = 6 + x(-7 + x(8 -|- x(4))). For x = 5, this
becomes 6 + 5(-7 + 5(8 + 5(4))). Whereas evaluating the polynomial P(x) in
its original form required nine operations, in this last form only six operations
are required — three multiplications and three additions.
In the general case of a polynomial of degree n, we note that all terms of
degree 1 or more contain a factor of x, so we can factor it out to get our
polynomial in the following form:
We repeat this process, starting with the term ai, to represent our polynomial
as:
By continuing to factor out an x from the terms after the constant term, we
finally arrive at the result:
than n — we use xan for b. Thus the code for poly-value in Program 5.13
1,
The last thing we illustrate before thinking about the representation of the
polynomial is how to build a given polynomial. For example, if we want to
define pi to be the polynomial
(define degree
(lambda (poly)
(subl (length poly))))
(define leading-coef
(Isimbda (poly)
(car poly)))
(define rest-of-poly
(lambda (poly)
(cond
((zero? (degree poly)) the-zero-poly)
((zero? (leading-coef (cdr poly)))
(rest-of-poly (cdr poly)))
(else (cdr poly)))))
(define poly-cons
(lambda (deg coef poly)
(let ((deg-p (degree poly)))
(cond
((and (zero? deg) (equal? poly the-zero-poly)) (list coef))
((>= deg-p deg)
(error "poly-cons: Degree too high in" poly))
((zero? coef) poly)
(else
(cons coef
(append (list-of -zeros (subl (- deg deg-p)))
poly)))))))
we simply write
(define pi (poly-cons 3 5
(poly-cons 2 -3
(poly-cons 1 1
(poly-cons -17 the-zero-poly)))))
Using the concept of data abstraction again, we have been able to develop
a symbolic algebra of polynomials without knowing how the polynomials are
represented. We did this by assuming that we had the selector and constructor
where we enter a zero when a term of a given degree is missing. The degree
of the polynomial is then one less than the length of the list of coefficients.
element is the degree of a term, and the second element is the coefficient of
that term. The pairs corresponding to terms with zero coefficients are not
included, except for the-zero-poly, which is represented by ((0 0)). The
pairs are ordered so that the degrees decrease. Thus the polynomial
(define degree
(lambda (poly)
(caar poly)))
(define leading-coef
(lambda (poly)
(cadar poly)))
(define rest-of-poly
(leunbda (poly)
(if (null? (cdr poly))
the-zero-poly
(cdr poly))))
(define poly-cons
(lambda (deg coef poly)
(let ((deg-p (degree poly)))
(cond
((and (zero? deg) (equal? poly the-zero-poly))
(list (list coef)))
((>= deg-p deg)
(error "poly-cons: Degree too high in" poly))
((zero? coef) poly)
(else
(cons (list deg coef) poly))))))
Exercises
Exercise 5.9
Implement the algebra of polynomials in the two ways indicated in the text.
For each implementation, test each of the procedures p+, p-, and p* with the
polynomials
P2(z) = x^ + 6x^ - 3z
Exercise 5.10
Look closely at the definition of p+ (see Program 5.9). When nl is greater than
n2, the variables a2 and rest2 are ignored. Similarly, when nl is less than
n2, the variables al and restl are ignored. Rewrite p+ so that this wasting
of effort disappears. Hint: You will need to use let within the consequents
of cond clauses.
Exercise 5.12
Another representation of polynomials eis lists that can be used is a list of
coefficients in the order of increasing degree. The list of pairs representation
given above can also be written in order of increasing degree. Consider the ad-
vantages and disadvantages of these representations compared to those given
above.
Exercise 5.13
How would the constructors and selectors be defiined if we use
Exercise 5.14
The definition oft* in Program 5.10 is flawed. Each time t* is invoked recur-
sively it evaluates both (degree trm) and (leading-coef trm), although
these values never change. In addition, the variable trm does not need to be
passed to t* because trm never changes. Explain how these two flaws are
eliminated in the following definition of p*.
represents the digit 1, and if it is off, it represents the digit 0. The information
contained in one such switch is called a bit, and eight bits of information
The 10 is called the base of the number system. We can think of any number
represented in this way as a polynomial in which the variable x has been
replaced by 10. In the same way, we can represent any number as a polynomial
in which the variable x has been replaced by the base b and the coefficients are
taken to be numbers between and 6—1. It is customary to write a number
in the base b system using the placeholder concept as a string of digits, each
digit being the corresponding coefficient in the polynomial. For example, for
base 2 {binary numbers), the digits are and 1 and the polynomial
1 X 2^ + 1 X 2^ + X 2^ + X 2^ + 1 X 2^ + X 2^ + 1 X 2°
an2" + an_i2''-^ + • • •
+ ai2 + oq
is written in the form a^an-i . . • aiao. We first consider the problem of finding
the decimal number when we are given its binary representation. This is
precisely the problem of evaluating a polynomial when the variable x has the
value 2. We can use the results of the last section if we represent our binary
number as a polynomial of degree n that has the coefficient ojfe for the term of
degree k, for A; = 0, 1, .., . n. We digits->poly that takes
define a procedure
a list of the digits of a binary number as its argument and returns a polynomial
of degree one less than the number of digits and that has the given digits as its
coefficients. The polynomial is constructed by the local procedure make-poly,
which has as its parameters the degree deg of the polynomial, which is one less
than the number of digits in the binary number, and Is, which is the list of
digits of the binary number. If we already have the polynomial for the binary
number obtained when the first digit is removed, that is, for parameters (subl
deg) and (cdr Is), we get the polynomial for the parameters deg and Is by
adding the term having degree deg and coefficient (car Is). This leads us
to the definition given in Program 5.16.
(define digits->poly
(lambda (digit-list)
(if (null? digit-list)
(error "digits->poly: Not defined for the empty list")
(letrec
((make-poly
(lambda (deg Is)
(if (null? Is)
the-zero-poly
(poly-cons deg (car Is)
(make-poly (subl deg) (cdr Is)))))))
(make-poly (subl (length digit-list)) digit-list)))))
(define binary->decimal
(lambda (digit-list)
(poly-value (digits->poly digit-list) 2)))
Consider (rest-of-poly poly); if its degree is one less than the degree of
poly, then (poly->digits poly) is just
(define poly->digits
(lambda (poly)
(letrec
((convert
(lambda (p deg)
(cond
((zero? deg) (list (leading-coef p)))
'
Otherwise, we have to cons zeros onto the list to take into account the gap
in the degrees between the leading term and the next term with nonzero co-
efficient. In order to do this, it is convenient to introduce a local procedure,
which we call convert. It keeps track of the degree of the term being con-
sidered, even if the coefficient is zero. Thus convert has two parameters: p,
which is a polynomial, and deg, which is an integer representing the degree
of the term. We define poly->digits £is shown in Program 5.18.
We also want to convert from the decimal number to its binary represen-
tation. We shall do this with the procedure decimal->binaxy, which takes a
decimal number and returns a list of the digits in its binary representation.
We can easily derive the algorithm if we recall that we want to find the co-
efficients an, On-i, ... ,ao in the polynomial corresponding to the number q,
Observe that ao, which must be either or 1, is just the remainder tq when
q is divided by 2, since q = ao + 2{somenuTnber). Recall that tq is if 9 is
49
24 1
12
6
3
1 1
the remainder when the previous quotient is divided by 2. The binary repre-
sentation of the number is found by reading the remainders from the bottom
of the table to the top: 11000101. We will then have
(decimal->binary 197) =» (1 1 1 1)
We now have
Two other number systems that are commonly used in computing are the oc-
tal (base 8) and the hexadecimal (base 16) systems. For octal, the base b in the
polynomial representation is replaced by 8, and in hexadecimal, the base 6 is
(define decimal->binary
(lambda (num)
(letrec
((dec->bin
(lambda (rI deg)
(if (zero? n)
the-zero- poly
(P+ (make -term deg (remainder n 2))
(dec- >bin (quol,ient n 2) (addl deg)))))))
(poly->digit£ (dec ->bin num 0)))))
.
replaced by 16. The digits 0,1,2,3,4,5,6,7 are used for octal numbers and the
digits 0, 1,2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F for hexadecimal numbers. Here
A stands for 10, fi for 11, . .
.
, F for 15.
Exercises
Exercise 5.16
Convert each of the following decimal numbers to base 2.
53
404
Exercise 5.17
Convert each of the following base 2 numbers to decimals.
a. 10101010
b. 1101011
n\un bl b2) is a list of digits that gives the base b2 representation of num.
Test your program on:
1 1 1 1
'
^
1
(1
1)
1
2 16)
1 1
=*
1
(1 7
0)
13)
6.1 Overview
In this chapter, we begin by taking a brief look at the string data type. We
then illustrate some of the input and output features available in Scheme by
developing a program to find the square root of numbers. After implementing
the basic square root algorithm, we look at ways of viewing intermediate
results and of providing data at run time. We close this chapter with a look
at two famous problems: the Tower of Hanoi and the Eight Queens problem,
both of which demonstrate ways of outputting data.
6.2 Strings
Strings form an important data type, and there are a number of operations
that can be performed on strings. A brief introduction to strings was presented
in Chapter 2. Recall that a string is written in Scheme sis a sequence of
keyboard characters enclosed within double quotes. We now look at a few of
the procedures in Scheme for manipulating strings. For example, the predicate
string? tests whether its argument is a string; string-length takes a string
where string is a given string, and start and end are integers satisfying the
inequalities < start < end < L where L represents the length of string. It
up to but not including the one with index end. Thus the length of the
substring is just the difference between end and start. It is also possible to
convert a symbol, such as 'hello into the string "hello" using the procedure
syinbol->string. Below are some examples illustrating string operations:
(string-length "") =»
(string-append "This is" " a string") =^ "This is a string"
(string-append "12" "34" "56") => "123456"
(substring "This is a string" 4) ==» "This"
The predicate string=? tests whether two strings are the same and distin-
guishes between upper- and lowercase. The predicate string-ci=? treats
upper- and lowercase as though they were the same character. (The "ci"
stands for case insensitive .) Thus the next to the last example above is false,
and the last example is true.
"We illustrate the use of these string procedures by defining a procedure
string-insert that inserts a string insrt into a string stmg so that the
first character in insrt has index n in the resulting string. For example,
(define string-insert
(lambda (insrt strng n)
(string-append
(substring strng n)
insrt
(substring strng n (stiring--length strng)))))
Exercises
(define substring-ref
(lambda (strng n)
(substring strng n (addl n))))
(lambda (x y)
(writeln x)
(writeln y)
(+ X y))
is the same as
(lambda (x y)
(begin
(writeln x)
(sriteln y)
(+ X y)))
Similarly, in the case of let and letrec, the body may consist of several
expressions so that
is the same as
Exercise
Exercise 6.4
An example of the use of implicit begins in cond clauses is given below:
(define mystery
(leunbda (pos-int)
(letrec ((helper
(lambda (n count)
(cond
((= n 1)
(newline)
(writeln "It took " count " steps to get to 1."))
((even? n)
(writeln count
". We divide " n " by 2.")
(helper (/ n 2) (addl count)))
(else
(writeln coiint
". We multiply " n " by 3 and add 1.")
(helper (+ (* n 3) 1) (addl count)))))))
(helper pos-int 0))))
In this example, each cond clause uses an implicit begin. What is the output
of (mystery 9)? Invoke mystery with a few other positive integer argu-
ments. Safe recursive programs contain a terminating condition which even-
tually halts the computation. No one has, eis yet, been able to demonstrate
that mystery is safe. Nor has a positive integer argument been found for
We have been using the keyboard to enter Scheme expressions, and we have
seen Scheme send output to the screen. The Scheme expressions we enter
its
are evaluated after we press the <RETURH> key, and then the result is printed
out. There are programs in which we would like to enter additional data while
the evaluation is taking place or in which we would like to print out not only
the final result but some intermediate results of the computation.
As an example illustrating the desire to see intermediate results, we look at
a program to compute the square root of a number by the method of successive
averaging known as Newton's method. If we have an estimate u for the square
root of a positive number a, a better estimate is always given by the average
of u and ^, that is, by v where^
-^("+^) (1)
Suppose we start with the estimate u = 1 and use formula (1) to compute v.
We then substitute this value v for u in (1) to get the next value v. We continue
this process until we are satisfied with the value we get: but what criterion
should we apply to decide that we are satisfied? We decide how many decimal
places we want in the answer, say five, and stop when we get two successive
estimates that are the same to five decimal places. In particular, we shall stop
the calculation when u and v differ by less than the agreed-upon tolerance,
0.000005. To test whether u and r are closer than the given tolerance, we
shall use the predicate close-enough? defined by writing
(define close-enough?
(laabda (u v)
(< (abs (- u v)) tolerance)))
^ To make this plausible, recall that we axe looking for the number s having the property
that 5 X s = a. If the estimate u is too large, then — is too small, and their average v is a
better approximation to s. Similarly when the estimate u is too small, — is too leirge, emd
their average i; is a better approximation to s.
(define square-root
(lambda (a)
(letrec
( (next-estimate
(leunbda (u)
(let ((v (/ (+ u (/ a u)) 2)))
(if (close-enough? u v)
V
(next-estimate v))))))
(next -estimate 1))))
2. If u is an estimate for the square root of a, then the next estimate is given
by the v calculated in (1).
In [2], [4], and [6], we called the built-in square root procedure sqrt to
compare its (presumably correct) results with our approximation. We see that
we got more than just five-decimal-place accuracy. This averaging method is
known to halve the error until the error is less than 1 in absolute value and
then to double the number of decimal-place accuracy with each successive av-
eraging. In order to see that this is actually happening, it would be interesting
[7] (begin
(display "Is")
(display " ")
(display 1.4142)
(display " the square root of 2?"))
Is 1.4142 the square root of 2?
To put a space between the items being printed with display, we have to print
a space using (display " "). Strings are printed without the double quotes.
If we want to print the string "the square root of 2?" on the next line,
we use the Scheme procedure newline, which takes no arguments and has the
effect of moving the cursor to the beginning of the next line. Scheme does not
specify a value for newline, and we again assume that our implementation
does not print a value for newline. We now use newline to print the string
"the square root of 2?" on the next line instead of on the same line as
[8] (begin
(display "Is")
(display " ")
(display 1.4142)
(newline)
(display "the square root of 2?")
(newline))
Is 1.4142
the square root of 2?
(define square-root-display
(lambda (a)
(letrec ((next-estimate (lambda (u)
(let ((v (/ (+ u (/ a u)) 2)))
(if (close-enough? u v)
V
(begin
(display v)
(newline)
(next-estimate v)))))))
(next-estimate 1))))
We now define the square root procedure using display to print the inter-
mediate results on the screen (Program 6.3).^ When we call square-root-
display, we get the output shown in Figure 6.4. We can now observe that
the convergence of the square-root algorithm does display the convergence
behavior described.
we have only asked for five-decimal-place accuracy in the answer, it
Since
would be more appropriate to print the final answer to five places. This can be
done using the Scheme procedure round, which rounds a number to the closest
integer. If we have a number like the one obtained for (square-root 2), we
round it to five places by first multiplying it by l.Oe+5 (i.e., 100000.0), to get
141421.356237469. This result is then rounded to the nearest integer using
round to get 141421, and to get the final answer, we divide by l.Oe+5, yielding
the result 1.41421, which is correct to five decimal places. In Program 6.5, we
round-n-places that has as parameters an integer
define a general procedure
n and a decimal number dec-num and returns the number rounded off" to n
decimal places.
Then to get the answer to (square-root 2) rounded off" to five decimal
places, we write:
^ Although the invocation of the local procedure next -estimate is within the begin ex-
is still an iterative program, since the value of the recursive invocation of
pression, this
next-estimate is returned directly as the value of next-estimate.
(define round-n-places
(lambda (n dec-num)
(let ((scale-factor (expt 10 n)))
(/ (round (* dec-num scale-factor)) scale-factor))))
(define round-5-places
(lambda (dec-num)
(round-n-places 5 dec-num)))
(if (close-enough? u v)
(begin
(newline)
y)
(begin
(display v)
(display " ")
(next-estimate v)))
Here, (display v) prints the value of v to the screen; then (display " ")
[12] (squcire-root-display 2)
1.5 1.41666666666667 1.41421568627451
1.41421356237469
The procedure display prints the string "the squcure root of 2?" with-
out double quotes. For the occasions when we do want the double quotes to
be printed in addition to the string, we can use the Scheme procedure write
instead of display. If we use write instead of display in [8], we get
[13] (begin
(write "Is")
(write " ")
(write 1.4142)
(newline)
(write "the squ<u:e root of 2?")
(newline))
"Is"" "1.4142
"the square root of 2?"
We also see that (write " ") prints a blank space with double quotes around
it, whereas (display " ") prints just the blank space.
invoked with no arguments, the computer stops and waits for an expression
to be entered from the keyboard. The value entered from the keyboard is
returned by (read). In Chapter 15, we shall see how read may be called
with one argument to read from a file instead of from the keyboard. In
Program 6.6, we illustrate the use of read by writing a simple program that
asks us to enter data, reads the data we enter, and then tells us what data
we entered. A statement written to the screen asking us to do something is
we write
(define greeting
(lambda ()
[14] (greeting)
Hello. How are you?
produced by the read expression. During the evaluation, the computer pauses
and waits for us to enter a datum from the keyboard, and it is that datum that
is bound to response. We chose to use write instead of display to print the
entered with double quotes, it is printed on the screen with the double quotes.
Had we used display instead of write, a string would be printed without the
double quotes. In order to stop the recursion, we enter done. The condition
terminating the recursion tests response to see if it is the same as done using
the predicate eq?. Here is a sample run using read-demo when a number,
a symbol, and a string are entered in response to the prompt asking for a
datum. The responses are presented in italics to distinguish them from the
prompts.
[16] (read-demo)
Enter data (enter done vhen finished) : 6.5432
You entered: 6.5432
Enter data (enter done when finished) : Hello
You entered: Hello
Enter data (enter done when finished) : "How are you?"
You entered: "How are you?"
Enter data (enter done when finished) : done
Thank you. Good-bye.
(define interactive-square-root
(lambda
,
(writ Bin "Enter the number whose square root you want
" or enter done to quit: ")
(let ((n (read)))
(if (eq? n 'done)
(writeln "That's all, folks .")
(begin
" " "
(writeln "The square root of n is (squeire--root n))
(newline)
(interactive-square--root))))))
n along with its square root to be sure that you did not make the mistake
of entering a wrong number when you responded to the prompt. Next the
procedure interactive-square-root is called, and the process is repeated
until thesymbol done is entered as a terminating condition. Below is an
example of an interactive session after calling interactive-square-root:
[1] (interactive-square-root)
Enter the number whose square root you want, or enter done to quit:
100
The square root of 100 is 10
Enter the number whose square root you want, or enter done to quit:
1000
The square root of 1000 is 31.6227766016838
Enter the number whose square root you want, or enter done to quit:
done
That's all, folks.
Exercises
Exercise 6.5
Write an interactive program that prompts for a number and then prints
the square and the square root of that number. It continues prompting for
1 76 Interactive Programming
:
numbers until stop is entered. The display should include the appropriate text
to identify the input and output.
and the user enters a number like 23.45. The program then tells how this
amount is made up of 100 dollar, 20 dollar, 10 dollar, 5 dollar and 1 dollar
bills and of quarters, dimes, nickels, and pennies. The output should say
something like:
Your change is
1 twenty-dollar bill
3 one-dollar bills
1 queurter
2 dimes
The program should then ask if you want change for another amount and
terminate if the answer is "no."
For example, for July 4, 1989, m = 5, d = 4, y = 89, and c = 19, while for
January 25, 1989, m = 11, d = 25, y = 88, and c = 19. The algorithm to
compute the day of the week is:
Write a program that prompts for the month, the day, and the year. The
month should be entered in the usual way with January as 1 and December
as 12. The year should also be entered in the usual way (e.g., 1989). The
program should then convert these data to what is needed by the algorithm
and compute the day. The output should be a statement such as "1/13/1989
is a Friday." The program should ask whether another day is desired and
These two problems, known by the names Tower of Hanoi and Eight Queens,
are included in this chapter to illustrate how information is displayed while the
program is running. The Tower of Hanoi^ problem was apparently originated
by the French mathematician Edouard Luccis in the nineteenth century. The
following story is told in connection with the problem:
left), C (for center), and R (for right), and n disks of decreasing diameter (in
going from bottom to top) all on the left post. Our goal is to move the n
disks to the right post, so that at the end they will again be stacked in order
of decreasing diameter in going from bottom to top. The two rules are that
we can move only one disk at a time and we must never put a larger disk on
' For a fuller account of the Tower of Hanoi puzzle, see Hofstadter, 1985.
top of a smaller one. Figure 6.8 illustrates the initial configuration for three
disks.
This problem lends itself to a beautiful recursive solution. The idea is that
if we have solved it for moving n — 1 disks from a source post to a destination
post (making use of the third post £is a help post), we can immediately solve
it for moving n disks from a source post (say L) to a destination post (say R)
by making the following moves:
1. With n disks on the source post L, we use the fact that we know the solution
for n— 1 disks to move the top n— 1 disks to the post C. (In these moves,
the destination post R serves as the help post.)
2. Now the largest disk is the only one on the source post L and the destination
post R is empty. We move the largest disk from the source post L to the
destination post R.
3. Making use of the now-empty post L as a help post, we now move the n — 1
We have now solved the problem for n disks under the assumption that
we knew the solution for n — 1 disks. If we have only one disk (n = 1), the
solution is easy: one merely carries that one disk from the source post to the
destination post. This case serves as the base case for our recursion. To be
able to write the solution to this problem, let us represent a single move that
carries a disk from a post called source to a post called destination by a
pair (source destination). For example, the move that carries a disk from
L to C is denoted by the pair (L C). A list of these pairs gives a sequence of
moves; for example, ((L C) (L R) (C R)) says that we move the top
first
diskfrom post L to post C, then the top disk on post L is moved to post R,
and finally the top disk on post C is moved to post R.
The procedure to solve the Tower of Hanoi problem for n disks is called
(define toser-of-hanoi
(lambda (n)
... ))
(define tower-of-hanoi
(lambda (n)
(letrec
((move
(lambda (n source destination helper)
... ))))))
the solution is a list whose only member is the pair consisting of source and
destination. Thus we have
(define tower-of-hanoi
(lambda (n)
(letrec
( (move
(lambda (n source destination helper)
(if (= n 1)
(list (list source destination))
... )))))))
Now for any n > we make use of the three steps given above. Step
1, 1 tells
which produces a list of pairs giving the moves that carry the first n— 1 disks
from the source post to the helper post making use of the destination post.
We append to that list the list containing the single move from the source
(define tower-of-hanoi
(lambda (n)
(letrec
( (move
(leunbda (n source destination helper)
(if (= n 1)
(list (list source destination))
(append
(move (subl n) source helper destination)
(cons
(list source destination)
(move (subl n) helper destination source)))))))
(move n 'L 'R 'C))))
post to the destination post. The resulting list is then consed onto the list of
pairs produced by
which moves the n — 1 disks from the helper post to the destination post
making use of the source post. These three steps enable us to complete the
definition of the local procedure move:
(define tower-of-hanoi
(leu&bda (n)
(letrec
( (move
(lambda (n source destination helper)
(if (= n 1)
(list (list source destination))
(append
(move (subl n) source helper destination)
(cons
(list source destination)
(move (subl n) helper destination source)))))))
... )))
on the source L and with destination R with the help of the post C. Thus the
complete solution is in Program 6.9.
(display d))))
(lambda (n)
(letrec
( (move
(lambda (n source destination helper)
(if (= n 1)
(begin
(shoH-move source destination)
(newline))
(begin
(move (subl n) source helper destination)
(show-move source destination)
(display ", ")
Now to solve the Tower of Hanoi problem for three disks moving them from
the post L to the post R with the help of the post C, we enter
[1] (tower-of-hcinoi 3)
((L R) (L C) (R C) (L R) (C L) (C R) (L R))
This shows the seven moves that solve the problem for n = 3.
of them. The code to do this is shown in Program 6.10. With this new
definition, we get the following output:
[3] (display-tower-of-heinoi 3)
L ~> R
L —> C, R —> C
L ~> R, C ~> L
C ~> R, L ~> R
12 3 4 5 6 7 8
Let us number the columns from 1 to 8 going from left and the
to right
rows from 1 to 8 going from the bottom to the top. The data structure we
use to denote the positions of the 8 queens on the board is a list of integers
of length 8 in which the A;th integer in the list denotes the row of the queen
in the fcth column. This data structure is permissible since the nature of the
problem rules out the possibility of two queens being on the same column.
The queens illustrated in Figure 6.11 are represented by the position list
(57263148)
In general the last element in the position list denotes the row of the queen in
the eighth column, so we denote the positions of the three rightmost queens
on the board in Figure 6.11 by (1 4 8). We call a position list legal if none
* For those not familiar with the rules of chess, a queen attacks another piece if the queen
and the other piece are on the same horizontal, vertical, or 45-degree diagonal line.
(define legal?
(lambda (try legal-pl)
(letrec
((good?
(lambda (nes-pl up down)
(cond
((null? new-pl) #t)
(else (let ((next-pos (ceir new-pl)))
(and
(not (= next-pos try))
(not (= next-pos up))
(not (= next-pos down))
(good? (cdr new-pl)
(addl up)
(subl down)))))))))
(good? legal-pl (addl try) (subl try)))))
(define solution?
(lambda (legal-pl)
(= (length legal-pl) 8)))
(define fresh-try 8)
of the queens in the list attacks any other queen in the list. The position list
(define build-solution
(lambda (legal-pl)
(cond
((solution? legal-pl) legal-pl)
(else (forward fresh-try legal-pl)))))
Imagine that we have the position list (64 17538) and are about to
place the last queen on the chess board. The queen we placed most recently
was the 6. Now we discover that we cannot place the last queen, so it must
be that 6 was a bad choice. Because we try the positions in a column in
decreasing order, the next possibility is 5. We try this list with 5, 4, 3, 2,
and 1, and they all fail. When we decrease it one more time we get 0, so we
backtrack. This time we use 3 and (17 5 3 8). Neither 3, 2, nor 1 can
be used, so once again we backtrack. Once again we get 0. The indicates
that there are no queens that can be added, so we backtrack again. This
time we use 6 and (5 3 8). We discover that although we cannot use 6, 5,
4, or 3, we can use 2. Once the 2 has been placed in the position list, we
try to place another queen, starting in position 8. This means we look for
a five-position list from the possibilities: (8 2 5 3 8), (7 2 5 3 8), (6 2
6 3 8), (5 2 5 3 8), (4 2 5 3 8), (3 2 5 3 8), (2 2 5 3 8), and (1 2
5 3 8). If one of these works, then we will be looking for a six-position list
among eight possibilities. If none of these works, then we backtrack and try
to find a four-position list from the single possibility 1 and (5 3 8). This
process, which consists of moving forward toward a solution as far as possible
and then backing up when we have hit a dead end, terminates with a solution
if there is one and terminates with the empty list if there are no solutions.
To solve the Eight Queens problem, we use the three procedures: build-
solution, forward, and backtrack. When build-solution (defined in Pro-
gram 6.13) is called, we know that its argument is a legal position list, so that
termination follows if it is the correct length. If it is not the correct length,
we callforward with an attempt that may or may not be legal. This attempt
will use fresh-try (i.e., 8) and a legal position list.
The procedure forward (see Program 6.14) is always called with a try and
a position list. If the try is 0, then we know that we have tried all positions
in this column, and none of them works, so we must backtrack. If the try is
not a 0, then adding try might make a legal position list. If so, we invoke
build-solution with the new legal position list. If not, we try again.
(define forBard
(lambda (try legal-pl)
(cond
((zero? try) (backtrack legal-pl)
((legal? try legal-pl) (build-solution (cons try legal-pl)))
(else (forward (subl try) legal-pl)))))
(define backtrack
(lambda (legal-pl)
(cond
((null? legal-pl) '())
(else (forward (subl (ceir legal-pl)) (cdr legal-pl))))))
invoked, we know that its argument is either the empty list, or it is a legal
position that has shown no promise. If it is the empty list, that means we
list
have backtracked as far as is possible and could not find a solution. This is
the result when you solve the Three Queens problem on a 3 x 3 board. When
this happens, we have no solution. It is more likely, however, that the legal
position list has shown no promise. Hence, we sacrifice that position list and
try the next one. This is accomplished by subtracting one from the car of the
current position list.
With these three procedures, we can now produce a solution to the Eight
Queens problem.
From here, it is an easy step to get all solutions. Each time we get a solution,
we save it in a list and backtrack over it to get another solution, until there
are no solutions left. In the experiment below, we are interested just in the
number of solutions.
(letrec
((loop (Isuabda (sol)
(cond
((null? sol) '())
(else (cons sol (loop (backtrack sol))))))))
(loop (build-solution '())))))
[4] (length (build-all-solutions))
92
and
(define seeircher
(lambda (legal? solution? fresh-try)
(letrec
((build-solution
(lambda (legal-pl)
(cond
((solution? legal-pl) legal-pl)
(else (forscird fresh-try legal-pl)))))
(forseird
(lambda (try legal-pl)
(cond
((zero? try) (backtrack legal-pl))
((legal? try legal-pl)
(build-solution (cons try legal-pl)))
(else (forwaird (subl try) legal-pl)))))
(backtrack
(lambda (legal-pl)
(cond
((null? legal-pl) '())
(else
(forward (subl (ceir legal-pl)) (cdr legal-pl))))))
(build- all-solut ions
(leUttbda ()
(letrec
((loop (lambda (sol)
(cond
((null? sol) '())
(else (cons sol (loop (backtrack sol))))))))
(loop (build-solution '()))))))
(build-all-solutions) ) )
Exercises
Exercise 6.8
An interesting question we can ask is how many moves M„ are needed to
move a tower of n disks from the source post to the destination post. We can
get a simple equation satisfied by M„ if we recall that we first used M„_i
moves to move the top n — \ disks to the helper post, then we used one move
to carry the largest one from the source disk to the destination disk, and
finally we used Mn-\ moves to take the n — \ disks from the helper post to
the destination post. This leads to the difference equation Mn = 1 + 2M„_i.
We know that if
also we have only one disk on the source post, it takes
only one move to take it to the destination post. Thus M\ = 1. Then
M2 = 1 + 2Mi = 1 + 2, and M3 = 1 + 2M2 = 1 + 2 + 22. gj^^^ ^y induction
that Mn = 1 + 2 + 2^ + 2^ H h 2""^ Sum this geometric series for M„ by
Exercise 6.9
Write a program that solves the Tower of Hanoi problem for n disks and k
posts. All of the disks are initially on the first post. They should be moved
to the kih post with a minimum number of moves, placing no disk on top of
a smaller disk in the process.
2's, and 3's is called a good sequence if it does not contain two identical
subsequences that are adjacent. Thus (123231) is not good because (2 3)
appears twice cis adjacent subsequences. On the other hand, (1 2 3 2 1 3) is
a good sequence. Methods similar to those used in solving the Eight Queens
problem can be used to show that for any n, one can find a good sequence of
length n. Generate all good sequences of length n.
Exercise 6.12
Change the definition of build-all-solutions so that instead of building a
list of all the answers, it displays the answers, one per line. Redefine your
solution so that it displays five solutions per line. Redefine the previous
solution to display n per line.
Exercise 6.13
The backtrack trace represents the search for the first solution of the Seven
Queens problem. Show what would be printed if we traced the second solution.
You may solve this by hand or by modifying the program.
Exercise 6.14
A standard technique for improving the efficiency of programs is to remove
invocations of length. In the procedures used to solve the Eight Queens
problem, we did not integrate the solution? test into the program because
we wanted to give you a relatively general program for doing backtracking.
Given that all you are concerned about solving is the Eight Queens problem,
rewrite the set of procedures so that there are no length invocations.
Exercise 6.15
Sets of procedures can sometimes be combined. For example, we do not
need both the backtrack and f orwaord procedures. we combine these two If
(define blemks
(IcUDbda (n)
(cond
((zero? n) "")
(else (string- append " " (blanks (subl n) ))))))
Procedures as Values
Procedures that take numbers as arguments and have numbers as values are
called simple arithmetic operations. Examples are addition and multiplica-
tion. Procedures that take procedures as arguments and have procedures
as values are called higher-order operations. One use we will make of these
higher-order procedures will be to show how we define a procedure that com-
bines similar characteristics of several different procedures. The activity of
combining properties of several procedures is also known as abstracting over
procedures, a natural generalization of abstracting over data. Not only is the
dining out procedure that we discussed in the introduction to Part 1 an ab-
straction of the dining activity, but it can be made to work for other external
we feed the consuming procedure the activities of reading the menu and eat-
ing, then we will once again have the dining out procedure, but if we feed it
the activities of reading the marquee and watching a movie, we will have the
movie-going procedure.
A predicate tests whether a value is true or false. Quantifiers tell us whether
some or all objects satisfy a predicate. Set theory is about collections of
elements and the properties of operations over such collections. In Chapter 8,
we develop a set algebra with quantifiers using higher-order procedures. The
essence of reasoning with higher-order procedures is the point of this part.
7.1 Overview
(define map
(lambda (proc Is)
(if (null? Is)
'()
produce a new list that is obtained from the old by adding 1 to each item in
the list, so that in our example, we would get (2468 10). We can define
a procedure addl-to-each-item that takes a list Is and returns the new list
(define addl-to-each-item
(lambda (Is)
(if (null? Is)
'()
the procedure we wish to apply to each element and the list. There is a
Scheme procedure map that has the parameters proc and Is and returns a
list that contains those elements that are obtained when the procedure proc
We can also apply map with a procedure that operates on lists as its first
(define for-each
(lambda (proc Is)
(if (not (null? Is))
(begin
(proc (car Is))
(for-each proc (cdr Is))))))
Observe that the elements of the list making up the second argument to map
must be of the correct type for the procedure that is applied to them. In
the first of these two examples, proc is a procedure that takes a list as its
argument and conses the symbol a onto the list. Thus each element of the
second argument to map is a list, and the list that is returned consists of
sublists, each of which begins with the a that was consed onto it.
There are procedures, such as display, that produce side effects of interest
to us rather than their returned values. If we apply such a procedure to each
item in a list, the list that is returned is not what interests us but only the side
effects produced by the procedure. In such cases, we use the Scheme procedure
to each item in the list, the desired side effects are produced, and the value
that is returned is unspecified, that is, it depends upon the implementation
ofScheme being used. A definition of for-each is given in Program 7.2. An
example using for-each is:
We shall see several more examples of the use of for-each below. But first
we introduce the form of lambda that is used to define a procedure that takes
an arbitrary number of arguments. We use this unrestricted lambda to define
the procedures writeln and error, which we have been using.
In a lambda expression, the keyword lambda is followed by a list of param-
eters. Its syntax is
where zero or more parameters are in the list of parameters and where the
number of arguments passed to the procedure, which is the value of this
lambda expression, must match the number of parameters. The body of the
lambda expression consists of one or more expressions, which are evaluated in
order and the value of the last one is returned. Suppose we want to define a
procedure add that can be applied to arbitrarily many numbers and returns
their sum. For example, we would like to have
(add 1 3 5 7 9) =» 25
(add 1 3 5 7 9 11) ==* 36
(add 1 3 5 7 9 11 13) =» 49
If the operands operandi . . . have the values argi . . ., then the variable var is
bound to the list of arguments (argi . .). The expressions expri expr2 . .
(define add
(letrec ((list-add
(laabda (Is)
(if (null? Is)
(define writeln
(lambda arge
(for-each display <irgs)
(newline)))
(define error
(lambda args
(display "Error:")
(for-each (leunbda (value) (display " ") (display value)) eirgs)
(nevline)
(reset)))
The general strategy for using this form of lambda is to remember that eurgs is
a list, so we define a local procedure list-add that takes a list as its argument
and let it do what we want add to do. Then we call list-add with the list
Two procedures, writeln and error, can also be defined using the unre-
stricted lambda. These are shown in Programs 7.5 and 7.6. The procedure
of no arguments reset in Program 7.6 returns the user to the prompt. Many
implementations of Scheme provide the procedure reset. A discussion of the
concepts used to define reset is given in Chapter 16. (See Exercise 16.6.)
Suppose we now want to find the maximum of two numbers in a list Is.
We cannot invoke (max Is), since the list Is is not the correct data type for
an argument to max, which expects each of its arguments to be a number.
If Is were, for example, (2 4), we would be looking at the expression (max
'(2 4)), which has the wrong type of argument for max. We could write
a recursive program that would compute the maximum of the values in Is.
(define add
(lambda args
(if (null? args)
where the procedure proc takes the same number of arguments as the number
of items in the list Hst-of-items. It returns the value obtained when we invoke
proc with the items in list-of-items as its arguments. For example, we can
call
The use of apply gives us another way to define procedures using the un-
restricted leunbda. Program 7.7 illustrates it by redefining the procedure add
given in Program 7.3, this time using apply in the recursive invocation of add
on the list (cdr args). There add is defined to apply to an arbitrary number
of numbers, so it cannot be applied directly to (cdr aorgs), which is a list
of numbers. Thus we use apply to invoke add on the items in the list (cdr
args).
The Scheme procedures + and are also defined to take an arbitrary number
of arguments. Thus we have:
(+ 1 3 5 7 9) => 25
(+ 5) => 5
(+) =>
(* 2 4 6) =« 48
( 5) ==» 5
(*) => 1
(define compose
(la«bda (f g)
(laabda (x)
(f (g x)))))
(laabda (x)
(f (g x)))
The procedure k so defined first takes the square root of its argument and
then adds one to the result; that is, A:(x) = ^Jx-\- 1. Thus k is quite a diff"erent
function from h.
Exercise
Exercise 1.1
What operand do we pass to k to get the same value eis (h 8)?
y times. This leads to Program 7.9. Similarly, using the fact that multipli-
cation of positive integers can be considered as repeated addition, times can
be defined in terms of plus and subl as shown in Program 7.10. This says
that multiplication of positive integers x and y is the same as adding x to
itself y times. In the same way, we can consider raising x to the exponent y
eis multiplying x by itself y times, so we can write the procedure exponent cis
(define plus
(Icimbda (x y)
(if (zero? y)
X
(addl (plus X (subl y))))))
(define times
(lambda (x y )
(if (zero? y)
(define exponent
(Icimbda (x y)
(if (zero? y)
1
(define super
(lambda (x y)
(if (zero? y)
1
The three procedures we have defined follow a simple pattern. Using this
pattern, we can define another procedure, which we call super, that uses
exponent and subl, as shown in Program 7.12. What does super do? Let us
evaluate (super 2 3).
(define superduper
(lafflbda (x y)
(if (zero? y)
1
(a tower of 4 twos), which is 65,536. We see that super yields large numbers
even with relatively small arguments like 2 and 4.
We now go to the next step and define superduper using super ajid subl, as
shown in Program 7.13. Then (superduper 2 3) is (super 2 4) or 65,536,
and (superduper 2 4) is (super 2 65536), which is a tower of 65,536 twos.
This is a very large number.
We can continue defining successive procedures by this process, but we must
make up a new name for each one. It would be better to define a procediire
(define ackermemn
(lanbda (n)
((super-order n) n n)))
Then
To get an estimate of how large (ackermann 4) is, we first note that 4'* is
256. To estimate 4^ = 4^^®, we set z = 4^"® and take the logarithm to get
logioz = 256logio4 = 154.13. Thus we get 4^^^ % 10^^* as our estimate for
4"* . Finally we estimate
Exercises
Exercise 7.5
In the following experiment, fill the blanks with the values of the expressions.
(map-first-tHO + (2 3 4 5
' 7) ) => (5 7 9 12)
(map-first-two max '(2 4 3 5 4 1)) => (44554)
two elements of the preceding list replaced by the value obtained when proc
is applied to them. When the list is reduced to containing only two elements,
the value returned is the value of proc applied to these two elements. If the
original list Is contains fewer than two elements, an error is reported. Here
is how the successive stages in the reduction look when proc is + and Is is
(3 5 7 9):
(3 5 7 9) -> (8 7 9) ^ (15 9) -* 24
(define andmap
(lambda (pred Is)
(reduce (lambda (x y) (and x y)) (map pred Is))))
where proc is a procedure that takes n arguments and each of the n lists has
the same length. This generalizes the procedures map and map2 given above.
(define map
(leunbda args
(let ((proc (car args)))
(letrec ((map-helper
(lambda (a*)
(if (any-null? a*)
•()
(cons
(apply proc (.map car a*))
(map-helper (.map cdr a*)))))))
(map-helper (cdr args))))))
(define ormap
(lambda (pred Is)
(if (null? Is)
#f
(or (pred (car Is)) (ormap pred (cdr Is))))))
What does this version of map return when the n lists are not of equal length?
7.3 Currying
The procedure + takes two numbers as arguments and returns their sum. The
procedure addl adds 1 to its argument. We can also define a procedure addS
that adds 5 to its argument by writing
(define add5
(lambda (n)
(+ 5 n)))
This can clearly be done for any number in place of 5. Another way of
approaching this problem makes use of the fact that a procedure may return
another procedure as its value. We can define a procedure curried+ that has
only one parameter, m, and returns a procedure having one parameter n, that
adds m and n:
(define curried+
(lambda (m)
(lambda (n)
(+mn))))
((curried* 5) 7) ^ 12
and we clearly can do the same for any other number in place of 8. What
underlies this method is the fact that we can take any procedure that has two
parameters, say x and y, and rewrite it as a procedure with one parameter
X that returns a procedure with one parameter y. The process of writing a
procedure of two parameters as a procedure of one parameter that returns
a procedure of one parameter is called currying the procedure.^ It is often
advantageous to use a curried procedure when you want to keep one argument
fixed while the other varies, so in essence, you are using a procedure of one
argument.
We next use currying to rewrite the definitions of four procedures in a way
that demonstrates certain common structural features that they possess. In
the next section, we shall abstract these common features and write a single
procedure from which the original four and many others can be obtained. The
four procedures are member?, map, sum, and product.
The procedure member? can be defined as follows:
(define member?
(Icunbda (item Is)
(if (null? Is)
»f
(or (equal? (car Is) item)
(member? item (cdr Is))))))
It tests whether the object item is a top-level object in the list Is. We are
going to apply the procedure member? with the same object item but different
lists Is, so we define the curried procedure member?-c, which is a procedure
with parameter item and returns a procedure that has the parameter Is and
tests whether item is a top-level member of Is. We do that in Program 7.16.
2. The procedure member?-c returns a procedure helper that has one param-
eter Is.
^ Conceived by Moses Schonfinkel in 1924 (See Schonfinkel, 1924) and neimed after the
logician Haskell B. Curry.
(define member?-c
(lambda (item)
(letrec
((helper
(lambda (Is)
(if (null? Is)
#f
(or (equal? (car Is) item) (helper (cdr Is)))))))
helper)))
We can now define the original procedure member? in terms of member ?-c
by writing
(define member?
(lambda (a Is)
((member?-c a) Is)))
(define map
(lambda (proc Is)
(if (null? Is)
'()
(define apply-to-all
(lambda (proc)
(letrec
( (helper
(lambda (Is)
(if (null? Is)
'0
(cons (proc (c«ir Is)) (helper (cdr Is)))))))
helper) )
(define sum
(letrec
( (helper
(lambda (Is)
(if (null? Is)
helper)
(define product
(letrec
( (helper
(lambda (Is)
(if (null? Is)
1
(* (cai Is)
•
(helper (cdr Is)))))))
helper)
(define map
(lambda (proc Is)
((apply-to-all proc) Is)))
We next look at two more procedures that take lists as arguments. The first,
(define swapper-m
(laMbda (x y)
(letrec
((helper
(laabda (Is)
(cond
((null? Is) '())
((equal? (car Is) x) (cons y (helper (cdr Is))))
((equal? (car Is) y) (cons X (helper (cdr is))))
(else (cons (cao: Is) (helper (cdr Is))))))))
helper)))
sum, assumes that the objects in the list are numbers and returns the sum of
the numbers in the list, and the second, product, assumes that the objects
in the list are numbers and returns the product of the numbers in the list.
We write their definitions in Programs 7.18 and 7.19 in such a way that they
demonstrate the same structure as the preceding definitions of member?-c
and apply-to-all. We could have written the procedures sum and product
without theletrec expressions, but we have chosen to do it this way to be
able tocompare the structure of these two procedures with the structure of
member?-c and apply-to-all when we abstract this structure in the next
section.
We close this section with an example that is similar to currying, this time
modifying a procedure with three parameters to get a procedure with two
parameters that returns a procedure with one parameter. We look at the
procedure swapper introduced in Program 2.8. Its definition is:
(define swapper
(laabda (x y Is)
(cond
((null? Is) '())
((equal? (car Is) x) (cons y (swapper x y (cdr Is))))
((equjd? (car Is) y) (cons x (swapper x y (cdr Is))))
(else (cons (car Is) (swapper x y (cdr Is)))))))
Exercises
Exercise round-n-places
7.14-'
rameters, n and dec-num, and returned the number dec-num rounded off" to
n decimal places. Rewrite the definition of round-n-places so that it takes
one parameter, and returns a procedure with one parameter, dec-num, that
n,
rounds the number dec-num off" to n decimal places. We can then write
to get the procedure that rounds a given number off" to five decimal places.
procedures has only one parameter. That is, between?-c has the parameter
X and returns a procedure that has the parameter y, which in turn returns
a procedure with the parameter z, that tests whether y is strictly between x
and z. Test your procedure on:
(((betHeen?-c 5) 6) 7) =^ #t
(((betBeen?-c 5) 5) 7) => »f
(((betHeen?-c 5) 4) 7) ==» #f
(define ormap
(lambda (pred Is)
((ormap-c pred) Is)))
(define is-divisible-by?
(leunbda (n)
(lambda (k)
(zero? (remainder n k)))))
whether it is not divisible by any of the odd integers from 3 to the largest odd
integer less than or equal to the square root of n. Why is it necessary only to
try integers less than the square root of n?
Exercise 7.21
Justify the statement "If we restrict ourselves to using only lambda expressions
having only one parameter in its list of parameters, we can still define any
procedure, regardless of how many parameters it has." Note that the currying
examples in this section show how to define procedures having two and three
parameters using only lambda expressions with one parameter.
In this section, we show how to abstract the structure of flatly recursive pro-
cedures to obtain a general procedure in terms of which the various special
cases can be defined. We illustrate this idea by looking for common structural
features in the four procedures member?-c, apply-to-all, sum, and product
defined in Section 7.3. A comparison of the code for these four procedures
(letrec
((helper
(lambda (Is)
(if (null? Is)
helper
to (car Is) and make the recursive call to helper on (cdr Is). We want
to define a procedure flat-recur that abstracts the structure of these four
programs; that is, it common features of these programs, and
embodies the
they can all be derived from it by using suitable parameters. Let us see how
much of flat -re cur we can write based on the above observations.
(letrec
( (helper
(Icu&bda (Is)
(if (null? Is)
))))
helper)))
How do we fill in the blanks? Let us first look at the blank that is the
consequent of the if expression. It is the action taken when Is is empty.
We call this consequent of the test (null? Is) the seed and denote it by the
variable seed. This will be the first parameter in the outer lambda expression.
Table 7.21 shows the seed for each of the four cases.
Procedure seed
inember?-c #f
apply-to-all
sum
product 1
The other blank in the if expression is in the action taken on (car Is) and
(helper (cdr Is)) when (null? Is) is false. The action taken on (car
Is) and (helper (cdr Is)) is a procedure that takes (car Is) and (helper
(cdr Is)) as arguments, and we call this procedure list-proc. We write
Is) ) to give us the alternative action when (null? Is) is false. For example,
the alternative action in the case of apply-to-all is
then
Procedure list-proc
member?-c (lambda (x y) (or (equal? x item) y))
apply-to-all (lambda (x y) (cons (proc x) y))
sum +
product
We are now ready to define the procedure flat-recur, which takes seed
and list-proc as arguments and produces precisely the procedure with pa-
rameter Is that abstracts the structure of the four procedures. (See Pro-
gram 7.23.) We can then write each of the four procedures in terms of this
new procedure. Furthermore, we can use it to write any procedure using
recursion on a list of top-level items.
We can now write the four procedures using flat-recur as follows:
(define member?-c
(lambda (item)
(flat-recuT #f (lambda (x y) (or (equal? i item) y)))))
(define apply-to-all
(launbda (proc)
(flat-recur '() (launbda (x y) (cons (proc i) y)))))
^ The procedure that we selected for list-proc in the case of meniber?-c does more pro-
cessing than is necessary, for it loses the benefit of the behavior of or. Generally when the
first argument to or is true, the value #t is returned without evaluating the second argu-
ment. However, when list-proc both arguments are first evaluated, and then
is called,
the or expression is evaluated, so the argument to which y is bound is always evaluated.
Even though the resulting version of member?-c is less efficient, it illustrates the principle
of procediiral abstraction and a feature that one should be aware of when applying it.
(define flat-recur
(lambda (seed list-proc)
(letrec
( (helper
(laabda (Is)
(if (null? Is)
seed
(list-proc (car Is) (helper (cdr Is)))))))
helper)))
You may be concerned that the procedure list-proc in these last two ex-
amples has a different structure from those in the first two examples. This is
really not the case, since we could also have used (lambda (x y) (+ x y))
in place of the variable +. and we could have used (lambda (x y) (* x y))
in place of the variable *.
The process we have used here looks for common features in several pro-
grams and then produces a procedure that embodies the code that is similar
in all of these programs. It enables us to express each of the original proce-
dures more simply. This process is called procedural abstraction. This is a very
powerful programming tool that should be exploited when it is applicable.
(lambda (x y)
(if (pred x)
(cons X y)
y))
(define filter-in-c
(lambda (pred)
(flat-recur
'()
(lambda (x y)
(if (pred x)
(cons X y)
y)))))
(define filter-in
(lambda (pred Is)
((filter-in-c pred) Is)))
Exercises
Is. It removes from the list Is all of its top-level elements that "pass" the
test, that is, it removes those top-level objects item for which (pred item)
is true. Write the definition of filter-out using a local procedure f ilter-
out-c that is defined using flat-recur.
The code for sum-all is presented in Program 7.27. Both of these procedures,
sum-all and filter-in-all-c, share the following lines:
(letrec
( (helper
(lambda (Is)
(if (null? Is)
helper
(define deep-recur
(lambda ( )
(letrec
((helper
(leunbda (Is)
(if (null? Is)
))))))
helper)))
(define f ilter-in-all-c
(Icunbda (pred)
(letrec
((helper
(lambda (Is)
(if (null? Is )
'0
(let ((a (car Is)))
(if (or (pair? a) (null? a))
(cons (helper a) (helper (cdr Is)))
(if (pred a)
(cons a lelper (cdr Is)))
(1-
if expression with test (null? Is). In the case of sum-all, seed is 0, and
in the case of f ilter-in-all-c, seed is (). We take seed to be the first
We can generate both of these using a procedure item-proc that has two
parameters, x and y. If we fill the blank with
then to get what we need for sum-all, we bind item-proc to +. To get what
we need for lilter-in-all-c, we bind item-proc to
(define deep-reciir
(lambda (seed item -proc list- proc)
(letrec
((helper
(leunbda (Is )
(if (null ? Is)
seed
(let ((a (car Is )))
(if (or (pair? a) (null? a))
(list-proc (h Blper a) (he Iper (cdr Is)))
(item-proc a (helper (cdr Is.)))))))))
helper)))
(lambda (z y)
(if (pred x)
(cons z y)
y))
anc
(define f ilter-in-all-c
(lambda (pred)
(deep-recur
'()
(lambda (z y)
(if (pred z)
(cons z y)
y))
cons)))
dural abstraction.
Exercises
The procedure remove-all-c takes an object item as its argument and re-
turns a procedure of the list Is, which removes all occurrences of item in Is.
The call (product-all Is) returns the product of all of the numbers in the
list of numbers Is. In both procedures, preserve the structure displayed in
the above definitions of siun-all and f ilter-in-all-c using letrec.
8.1 Overview
8.2 Quantifiers
We study various procedures in this section that we shall use later in our
discussion of sets. If we are given two items, these procedures are used to
tell whether both, at least one, or neither of the items satisfy some condition.
In a sense, they give an idea of how many of the items satisfy the condition;
hence these procedures are called quantifiers. In this section, we introduce the
quantifiers both, at-least-one, and neither; after sets have been introduced
in the next section, we add to this list the quantifiers f or-all, there-exists,
and none.
The first of these is a procedure both that has a predicate pred as its
parameter and returns another predicate that has two parameters, ajrgl and
arg2. It is true if and only if pred is true for both argl and arg2. It is easy
to write the definition of both:
(define both
(lambda (pred)
(lambda (zirgl arg2)
(emd (pred argl) (pred arg2)))))
For example, if we want to test whether two lists are both nonempty, we
can invoke the predicate
((both (lambda (Is) (not (null? Is)))) ' (a b c) '(d e)) => «t
Thus
(define neither
(Icunbda (pred)
(leimbda (argl 2u:g2)
(not (or (pred argl) (pred arg2))))))
Thus
(define at-least-one
(lambda (pred)
(lambda (argl arg2)
(or (pred argl) (pred arg2)))))
((at-least-one even?) 1 3) ^^ #f
((at-least-one even?) 1 2) ^=»» #t
We can play with logic a little to show that we can take one of these three
procedures as basic and express the other two in terms of it. For example, let
us take neither as the basic one and try to express both and at-least-one
in terms of neither. Given two lists, if we want to say that both lists are
empty, we can also say that neither of the lists is not empty. In general, saying
that both items satisfy a predicate is the same as saying that neither of them
does not satisfy the predicate. Thus we can write
(define both
(lambda (pred)
(leunbda (aurgl axg2)
((neither (lambda (arg) (not (pred arg)))) «irgl arg2))))
where corresponding arguments are exactly the same in both places. We can
often simplify the definition to be
which, according to the above simplification rule, has the same value as
(define both
(lambda (pred)
(lambda (eirgl eurg2)
((neither (compose not pred)) eurgl arg2))))
is the same as
(define both
(lambda (pred)
(neither (compose not pred))))
This kind of simplification was possible because we curried out the parameter
pred when we decided on the form of the definitions. This kind of simplifi-
cation actually leads to a definition that reflects the way we verbalize it in
(define at-least-one
(lambda (pred)
(lambda (eirgl arg2)
(not ((neither pred) argl arg2)))))
This says that at least one of the two satisfies the predicate when it is not
true that neither satisfies the predicate.
Exercises
Exercise 8.1
Show that the procedure both can be taken as the basic quantifier and that
the other two, neither and at-least-one, can be defined in terms of both.
Exercise 8.2
Start with at-least-one as the basic quantifier, and define neither and both
in terms of at-least-one.
In this section, we develop a data type called sets. We treat sets as an abstract
data type that has certain basic operators whose existence we first assume and
later implement when we choose a representation of sets. An implementation
of sets must begin with the specification of the kinds of objects that are
allowed as the basic building blocks. The collection of all such base objects is
We now consider how we implement sets. We denote the empty set by the-
empty-set. There is a predicate to test whether a set is empty: empty-set?.
We also use a predicate set? that tests whether an object is a set. The base
elements of our sets belong to some universe for which there is a sameness
predicate, which we assume is equal?.
We now introduce the selectors and a constructor for use with sets. There
are two selectors, pick and residue. The selector pick takes a set as its
argument and returns an element of that set. Since order of elements is not
meaningful in sets, we cannot say that pick selects the first member of a set.
If obj is an object and s is a set, then the invocation ((residue obj) s)
returns a set that contains all of the elements of s except obj . If obj is not
in the set s, then ((residue obj) s) returns the set s. The constructor
is called adjoin. If obj is an object and s is a set, then (adjoin obj s)
returns a set that contains obj and all of the members of s. With these basic
operators, we now proceed to develop additional operations on sets.
(define meike-set
(lanbda args
(letrec
( (list-meJce-set
(Icunbda (args-list)
(if (null? args-list)
the-empty-set
(adjoin
(car eirgs-list)
(list-make-set (cdr args-list)))))))
(list-meJte-set args))))
(define none
(laitbda (pre i)
(letrec
((test
(leunbda (s)
(or (empty--set? s)
(let ((elem (pick s)))
(and (not (pred elem))
(test , ((residue elem) s))))))))
test)))
((there-exists pred) s)
is when there is at least one element in s for which pred is true. We can
true
define there-exists in terms of none using the fact that there is an element
of s satisfying pred only when it is not the case that none of the elements of
s satisfies the predicate.
(define there-exists
(lambda (pred)
(Icuabda (s)
(not ((none pred) s)))))
Then the simplifying rule of Section 8.2 can be used to give us the following
form of the definition:
(define there-exists
(lambda (pred)
(compose not (none pred))))
of the elements in s only when there are no elements in s for which pred is
(define f or-all
(lambda (pred)
(lambda (s)
((none (lambda (x) (not (pred x)))) s))))
(define for-all
(leUDbda (pred)
(none (compose not pred))))
We can test for the sameness of objects or sets with a sameness procedure
called set-equal. We define set-equal so that it can be used to test for the
(define set-equal?
(lambda (objl obj2)
((set-equal objl) obj2)))
If objl and obj2 are base elements of a set, they belong to a universe, and
their sameness can be tested with the predicate equal?. On the other hand, if
the objects are sets, we use the criterion of sameness used in set theory. First,
(define set-equal
(lambda (objl)
(lambda (obj2)
(or (and ((neither set?) objl obj2)
(equal? objl obj2))
(and ((both set?) objl obj2)
((subset objl) obj2)
((subset obj2) objl))))))
help us. If we had the ordinary predicate set-equal?, with two arguments,
we wouldn't be able to create the obj-specific version so easily. We'd have to
say (lambda (s) (set-equal? s obj)) instead, and that's harder to read.
We then have
(define element
(lambda (obj)
(lambda (s)
((there-exists (set-equal obj)) s))))
Using the simplifying rule of Section 8.2, we may rewrite the definition of
element to be
(define element
(leunbda (obj)
(there-exists (set-equal obj))))
We now see that the last line is merely the composition of the two procedures,
of currying can enable us to express our ideas in more compact and convenient
form.
The invocation ((element obj) s) tests for the set-theoretic relation
obj G s
which says that the object obj is a member of the set s. The set-theoretic
relation
s 3 obj
which says that the set s contains the object obj as a member, is tested for
by the predicate (contains s). For example, b £ {a,b,c} and {a,b,c} 3 b.
(define contains
(lambda (set)
(lambda (obj)
((element obj) set))))
(define superset
(lambda (si)
(laabda (s2)
((for-all (contains sD) s2))))
(define subset
(lambda (si)
(lambda (s2)
((superset s2) si))))
The number of elements in a set is called the cardinal number of the set.
For example, the cardinal number of the set {a, 6, c, d} is 4, and the cardinal
number of the set {{a, b}, {c. d}, {e, /}} is 3, while each of the elements of this
set is itself a set with cardinal number 2. It is an easy matter to define the
procedure Ceurdinal, which determines the cardinal number of its argument
set. To do so, we use recursion on the elements of the set. The cardinal
number of the-empty-set is 0, which gives us our terminal condition. If the
set s is not empty, we pick out one element and compute the cardinal number
of the rest of the set. To get the cardinal number of s, we have to add 1 to
the cardinal number of the rest of the set. Here is the definition:
(define cardinal
(lambda (s)
(if (empty-set? s)
(define intersection
(leunbda (si s2)
(letrec
((helper
(lambda (si)
(if (empty-set? si)
the-empty-set
(let ((elem (pick si)))
(if ((contains s 2) elem)
(adjoin elen (helper ((residue elem) si)))
I
The intersection of two sets si and s2 is the set consisting of those ele-
ments of si that are also elements of s2. The intersection of si and s2 is
denoted by si n s2. For example, {a, 6, c, d}n {&, d, e} = {b,d}. We define a
procedure intersection that returns the intersection of its two arguments.
This definition uses recursion on the elements of si. Since s2 is not affected
in each recursive call, we helper that has only the
define a local procedure
one parameter si. When si is empty, the intersection is the-empty-set.
Otherwise, we select an element elem from si and take the intersection of
the rest of the set si with s2. If elem is contained in s2, we adjoin it to the
intersection of the rest with si. Otherwise, we simply return the intersection
of the rest with si. The definition is given in Program 8.14.
The union of the sets si and s2 is the set consisting of all of the elements
that are either in si or in s2. It is denoted by si U s2. For example,
{a, b, c, d} U {b, d, e} = {a, b, c, d, e}. We define a procedure union that takes
two sets as arguments and returns their union. We again use recursion on the
set si. This time, when si is empty, the union is s2. This is our terminal
condition. The recursion proceeds as in the case of intersection, but now
we want to adjoin elem to the union of the rest of the set si with s2 when
elem is not contained in s2. Thus we get the definition in Program 8.15.
The difference between the sets si and s2 is the set consisting of those
elements of si that are not in s2. denoted by si \ s2. For example,
It is
(define union
(lambda (si s2)
(letrec
((helper
(lambda (si)
(if (empty-set? si)
s2
(let ((elem (pick si)))
(if (not ((contains s2) elem))
(adjoin elem (helper ((residue elem) si)))
(help er ((residue el em) si))))))))
(helper 5l))))
(define difference
(lambda (si s2)
(letrec
((helper
(leunbda (si)
(if (empty-set? si)
the-empty-set
(let ((elem (pick si)))
(if (not ((contains s2) elem))
(adjoin elem (helper ((residue elem) si)))
(helper ((residue elem) si))))))))
(helper si))))
when the terminal condition is true. We call that set the base-set. And they
differ in the predicate that is applied to decide whether to adjoin the element
elem picked from the set si. We call that predicate pred. We use base- set
and pred as parameters to the procedure set-builder, which abstracts the
structure of the three preceding programs. (See Program 8.17.)
(define set-builder
(lambda (pred base -set)
(letrec
((helper
(lambda (s)
(if (empty-set? s)
base- set
(let ((elem (pick s)))
(if (pred elem)
(adjoin elem (helper ((residue elem) s)))
(helper ((residue elem) s))))))))
helper)))
(define intersection
(lambda (si s2)
((set-builder (contains 82) the-empty-set) si)))
(define union
(lambda (si s2)
((set-builder (compose not (contains s2)) s2) si)))
(define difference
(lambda (si s2)
((set-builder (compose not (contains s2)) the-empty-set) si)))
(define f aaily-union
( lambda (s)
(if (empty-set? s)
the-empty-set
(let ((elem (pick s)))
(union elem (family-union ((residue elem) s) ))))))
(define family-intersection
(lambda (s)
(if (empty-set? s)
the-empty-set
(letrec
((f am-int
(lambda (s)
(let ((elem (pick s)))
(let ((rest ((residue elem) s)))
(if (empty-set? rest)
elem
(intersection elem (feui-int rest))))))))
(f am-int s)))))
If the set S has as its members other sets, we can ask for the union of the
member sets. The union of the sets that are members of the set S is called
the family union of S. We represent it symbolically by (JS. For example,
U{{a. b}. {b. c, (f), {a, e}} = {a, 6, c, d. e}. We define (in Program 8.19) a pro-
cedure family-union that takes as its parameter a set s whose elements are
(define set-map
(lambda (proc s)
(if (empty-set? s)
the-empty-set
(let ((elem (pick s)))
(adjoin (proc elem)
(set-map proc ((residue elem) s)))))))
the same way as acts as an identity for plus (see Program 7.9) and 1
acts as an identity for times (see Program 7.10). For intersection, there
is no computable identity. Moreover, the-empty-set acts as an annihilator
for intersection in the same way that acts as an annihilator for x. We
must avoid passing the-empty-set to intersection. This is accomplished
by terminating the recursion when we reach a set that contains a single set.
The next procedure we define before looking into how we represent sets
is set -map, which takes two parameters, a procedure proc and a set s. It
returns the set consisting of those elements that are obtained when proc is
s) evaluates to the set {3,2, 1}. Similarly, if s is { — 1,0, 1}, then (set-map
addl s) evaluates to the set {0, 1,2}. We define set-map in Program 8.21.
(define list->set
(lambda (Is)
(apply make-set Is)))
In a similar way, we can ask for a procedure that takes the elements of a set
and builds a list containing those elements. This is done by picking elements
out of the set and consing them onto a list. Program 8.23 shows how the
procedure set->list can be defined.
We have now included enough of the procedures for manipulating sets for
(define set->list
(lambda (s)
(if (empty-set? s)
'()
you to get an idea of how to define set operations. We are now ready to
consider ways of representing sets. This is done in the next section.
Exercises
Exercise 8.4
In this section, we showed that there-exists and lor-all can be defined
in terms of none. Show that we could have taken there-exists as the basic
one and defined the other two in terms of show that we could
it. Similarly,
have taken f or-all as the basic one and defined the other two in terms of it.
(define for-one
(lambda (pred found-proc not-found-proc)
(letrec ((test
(lambda (s)
(if (empty-set? s)
(not-found-proc)
(let ((v (pick s)))
(if (pred v)
(found-proc v)
(test ((residue v) s) )))))))
test)))
((for-one
(lambda (x) (> x 7))
(lambda (v) (+ v 8))
(lambda () "Not found"))
(make-set 2 4 6 19 21 7))
returns the value 27 (or possibly 29, depending upon which element was se-
Exercise 8.6
Show, using a discussion analogous to that used with element in this section,
We have now defined the set procedures that enable us to manipulate sets as
a data type. These definitions all depend upon the six basic representation-
We make this distinction in order to define the predicate set? which will
determine whether its argument is a pair and its car is that unique tag. For
example, we represent the set {a,b, c} as the tagged list ("set" a b c). In
this first representation, we allow repeated elements in the lists. However, if
an element occurs once in a list, that element belongs to the set represented
by the and any other occurrences of that element in the list are ignored.
list,
(define empty-set?
(lanbda (s)
(eq? s the-eapty-set)))
(define set?
(leiHbda (<urg)
(and (pair? arg) (eq? (car arg) set-tag))))
(define pick
(Isjibda (s)
(let ((Is (cdr s)))
(if (null? Is)
(error "pick: The set is empty.")
(list-ref Is (random (length Is)))))))
the procedure random in defining pick in order to select some element from
the list. The procedure random takes a positive integer n as an argument and
returns some randomly selected integer k in the range < A: < n. We chose
to use a randomly selected element from the list rather than the car of the list
or any other specific element of the list in order to convey the idea that the
set is unordered. Random number generators are discussed in Footnote 1 of
Section 10.2.5 and the procedure described there is defined in Exercise 13.3.
The definitions in Program 8.25 of the remaining two basic procedures,
adjoin and residue, we allow repetitions
reflect the fact that in the repre-
Here are some examples of how the procedures behave using our represen-
tation that allows repetitions of items in the lists.
(define residue
(lambda (elem)
(lambda (s)
(let ((Is (remove elem (cdr s))))
(cond
((null? Is) the-empty-set)
(else (cons set-tag Is)))))))
(define adjoin
(lambda (elem s)
(cons set-tag (cons elem (cdr s)))))
Another representation for a set s is a list that contains the elements of s but
does not allow repetition of elements. The selector residue now has to remove
only the first occurrence of its first argument from the set since there are no
repetitions.Thus it uses remove-lst (Program 2.4) instead of remove. The
constructor adjoin must now test to determine whether its first argument,
the object, is already an element of its second argument, the set. It adds the
object to the set only if it is not already a member of the set. Program 8.26
shows the definitions of adjoin and residue for the representation with no
repetitions.
Here are some examples of how some of the procedures defined in Section 8.3
look when using the second representation of sets:
(define residue
(la>bda (elea)
(laabda (s)
(let ((Is (re«ove-lst elem (cdr s))))
'
(cond
((niill? Is) the-empty- set)
(else (cons set-tag Is )))))))
(define adjoin
(la»bda (el en s)
(cond
((e«ber? eles (cdr s)) s)
(else (cons set--tag (cons elem (cdr s) ))))))
You now might ask. "Which representation is better?" Each has its advan-
tages. For example, in the first representation, the selector residue does
more work than its counterpart in the second representation since it has to
remove all occurrences of the element that was picked, while in the second
representation, it only has to remove the first occurrence. In the second rep-
resentation, adjoin does more work since it has to check whether the element
to be added is already in the tagged list. The lists involving no repetitions
represent the sets more compactly. Thus there is a trade-off when choosing
between these two representations.
We have built into our universe, not only symbols, numbers, and booleans.
but any data for which equal? works, and that includes lists. Here aire some
examples using this tagged-list representation of sets where some elements are
lists.
=* ("set" (2 3) 1 2 (1 2) (3 4))
(union (make-set 1 (make-set 1 2) (make-set 2 3))
(make-set 1 2 (make-set 1 2) (make-set 3 4)))
=*• ("set" ("set" 2 3) 1 2 ("set" 1 2) ("set" 3 4))
Exercises
Exercise 8.7
Use this definition of pick to implement sets with lists having repetitions:
(define pick
(Icu&bda (s)
(car (cdr s))))
In the following exercises, use only operations on sets. Do not use operations
on sets that depend upon the representation of the sets.
Exercise 8.8
The procedures union and intersection defined in this section each took two
sets as arguments. Rewrite these definitions using the unrestricted lambda so
that both take an arbitrary number of sets as arguments. Test your procedures
(union
(make-set 12 3 4)
(make-set 13 4 5)
(make-set 2 1)) ==> ("set" 12 3 4 5)
(ake-set 13 4 5)
(ake-set 15 6 3 7))=^ ("set" 1 3)
Abstract the structure of these two definitions to get a procedure from which
tinion and intersection can both be obtained by passing the procedural
abstraction appropriate arguments.
(synaetric-dif ference
(ake-set 12 3 4 5)
(ake-set 3 4 5 6 7)) =» ("set" 1267)
Hint: Assume that power-set is defined for the rest of the set when an element
is picked out.
((select-by-cardinal 2)
(ake-set (ake-set 'a) (aake-set 'a 'b) (ake-set 'a 'b 'c)
As an application of sets, we show how ordered pairs and the Cartesian prod-
uct of two sets are defined and use these ideas to develop the logical concepts
of functions and relations. We present a development of ordered pairs based
upon the development of sets presented in the preceding sections.
An ordered pair is a pair of elements in which the order is significant; that is,
(x, y) and (y, x) represent different ordered pairs as long as x and y are not the
same. We again treat ordered pairs as an abstract data type and introduce
the basic operations that apply to ordered pairs and then look at possible
representations of ordered pairs. Ordered pairs have two selectors and one
constructor. There is a selector called op-lst that takes an ordered pair as
its argument and returns the first element in the ordered pair. Similarly, there
is a selector called op-2nd that returns the second element in the ordered pair.
Finally, there is a constructor that is called make-op such that (maie-op x
y) is the ordered pair containing x as its first member and y as its second
member. There is also a predicate op? that tests whether its argument is an
ordered pair.
We now consider ways of representing ordered pairs. We present three dif-
ferent representations in this section: sets, two-element lists, and dotted pairs.
Logicians usually start with sets and build other concepts from them. In our
first representation, we show how this can be carried out in Scheme. Com-
pared to the last two representations, the first is quite complicated. It shows
the natural advantage the list or dotted-pair representations have for repre-
senting ordered pairs. If you are not interested in the set theory development
of ordered pairs, you can skip over the next two paragraphs and go on to the
list and dotted-pair representations.
We first represent ordered pairs as sets. A naive first attempt would repre-
sent the ordered pair (x,y) with the set {x,y}. However, there is a problem:
if ^ y,
X then (x, y) 9^ (y, x), but {x, y} = {y, x} since order is immaterial in
sets. We can get around this difficulty by representing the ordered pair (x, y)
by the set {{x},{x,y}}. Then the ordered pair (y, x) is represented by the
set {{y}, {y, a:}}, which is not the same set as that used to represent (x,y),
as long as X is not the same as y. We identify the first element of the ordered
pair represented by {{x}, {x, y}} by noting that it is the only element in the
intersection {x} D {x, y} of the two member sets. Similarly, if x and y are not
equal, the second element of the ordered pair is the only element in the set
difference between Uii^)' i^' I/}} ^^^ flii^)' {^' y))- ^^ ^ ^^^ V ^^^ equal,
pick the first element of the ordered pair. It should be observed that the same
(define nake-op
(lambda (i y)
(maOce-set (make-set i) (make-set x y))))
(define op?
(lambda (set)
(and (set? set)
((for-all set?) set)
(= (cardinal (family-intersection set)) 1)
(define op-lst
(lambda (op)
(pick (family-intersection op))))
(define op-2nd
(lambda (op)
(let ((fam-int (family-intersection op) )
ordered pair is represented by {{z}, {x, y}}, {{z}, {y, z}}, and {{y, z}, {z}}.
Using this representation in terms of sets, the definitions of the four basic
procedures for ordered pairs are given in Program 8.27. Given the first element
X and the second element y of the ordered pair, the constructor maie-op
produces the ordered pair {{z}, {z,y}}. To understand the definition of op?,
observe that it is possible for the cardinal number of an ordered pair to be
equal to one. This is illustrated by {{a}, {a, a}}, which represents the ordered
pair [a, a). We have {{a}, {a, a}} = {{a}, {a}} = {{^}}i so its cardinal
number is one.
There are other ways of representing ordered pairs that we can also use.
(define make-op
(leuttbda (x y)
(list X y)))
(define op?
(lambda (Is)
(and (pair? Is) (pair? (cdr Is)) (null? (cddr Is)))))
(define op-2nd
(lambda (op)
(cadr op)))
(define make-op
(Izimbda (x y)
(cons X y)))
(define op?
(lambda (pr)
(pair? pr)))
(define op-lst
(lambda (op)
(car op)))
(define op-2nd
(lambda (op)
(cdr op)))
is, as (cons x y). We then have the definitions given in Program 8.29. The
definitions in Programs 8.28 and 8.29 can be simplified using the simplification
rule of Section 8.2. See Exercise 8.12.
The Cartesian product of the two sets Si and 5*2 is the set of all ordered pairs
(define caurtesiam-product
(lambda (si s2)
(if (empty-set? si)
the-empty-set
(let ((elem (pick si)))
(union (set-map (lambda (> (make-op elem x)) 82)
(cartesian-product (( residue elem) 81) 82))))))
{(a, d), (a, e), (6, d), (6, e), (c, d), (c, e)}
The set procedure that forms the Cartesian product of two sets is defined in
Program 8.30.
A relation R from a set X to a set Y is defined to be a subset of the
Cartesian product of the two sets X and Y Thus the relation R is
. a, set of
ordered pairs in which the first element is in X and the second element is
X is the set (make-set a b c) and Y is the set (make-set 1), then the
following is a relation from X to Y:
(define domain
(laabda (rel)
(set-aap op-•1st rel)))
(define rzmge
(lambda (rel)
(set -map op-•2nd rel)))
(define is-older-than-relation
(make-set (make-op 'tom 'bob)
(make-op 'tom 'jim)
(make-op 'bob 'jim)))
We can define a predicate is-older-tham? that has as its two parameters two
members bl and b2 of the set boys and returns true if the ordered pair (maJce-
op bl b2) is an element of the binary relation is-older-than-relation.
For example, we can write
(define is-older-than?
(lambda (bl b2)
((contains is-older-than-relation) (make-op bl b2))))
Suppose we are given a relation rel from one set to another. We now write
the definition of a procedure subrelation/lst that builds a new relation
consisting of all pairs from the relation rel that have a given element as their
the subrelation consisting of the two ordered pairs starting with tom is re-
Y in which no two ordered pairs with the same first elements have diff'erent
second elements. Since functions are relations, domain and range are already
defined for functions. This definition of a function as a set of ordered pairs
is we have been using throughout the book.
equivalent to the definition If
the range Y, this is the relation consisting of the ordered pairs (x,y) sat-
isfying y = f{x). With this view of functions, the factorial function is the
(define subrelation/lst
(lambda (rel)
(lambda (arg)
((set-builder
(lambda (x) ((set-equal (op-lst i)) arg))
the-empty-set)
rel))))
(define function?
(lambda (rel)
(or (empty-set? rel)
(let ((subrel ((subrelat Lon/lst rel) (op-lst (pick rel)))))
(and (= (cardinal subr<3l) 1)
(fiinction? (difference rta subrel)))))))
ordered pairs in rel that have the same first element as some ordered pair
(pick rel) in rel. If the number of distinct second elements in this subset is
ement in the ordered pair in / that has x as its first element. In Program 8.34,
we define a procedure value that takes a function fun as its parameter and
returns a procedure that takes as its parameter an element arg in the domain
of fun and returns the value of fun at airg.
and cdr. Here we have developed sets as a data type having the constructor
adjoin and the two selectors pick and residue. Using these, we defined many
procedures that manipulate sets. We used lists in several representations of
sets. We then proceeded to define ordered pairs using sets. Ordered pairs
are another data type having the constructor make-op and the two selectors
(define value
(lambda (fun)
(Icu&bda (eirg)
(op-2nd (pick ((subrelation/lst fun) arg))))))
op- 1st and op-2nd. These were used to define relations and functions on
sets. It is interesting to observe that if we start with sets as our bcisic data
type, we can use sets as we did to define ordered pairs, and then we can use
ordered pairs as a representation of lists. For any two elements, x and y, we
define (cons x y) to be (meOte-op x y). We then define car to be op-lst
and cdr to be op-2nd. The empty list () is represented by the-empty-set.
Using these definitions of cons, car. cdr, and (), we can proceed to define
all of the procedures on lists that were defined in the earlier chapters. Thus
we have come full circle. We can take sets as our basic data type and develop
lists in terms of sets, or we can take lists as our basic data type and develop
sets in terms of lists.
Exercises
Exercise 8.12
In Programs 8.28 and 8.29, we can rewrite the definition of op-lst as (de-
fine op-lst caoc) using the simplifying rule of Section 8.2. Redefine all of
the procedures in Programs 8.28 and 8.29 for which the simplifying rule is
applicable.
In the following exercises, use the set operations developed in this chapter.
Make your programs independent of the representation of sets being used.
Many of the problems in this list use the results of previous problems, so do
them in order.
{(1, 2), (1, 3), (2, 3)}. Define the procedure make-relation.
inverse relation. See Exercise 8.14. Define a predicate S3rmmetric? that tests
whether a given relation rel is symmetric.
(trEuisitive?
(make-relation '(1 2) '(1 3) '(1 4) '(2 3) '(2 4) '(3 4))) => #t
(transitive?
(make-relation '(0 0) '(1 1) '(2 2) '(3 3) '(4 4))) => #t
(transitive?
(make-relation '(1 1) '(12) '(3 2) '(2 1))) ==*> #f
(equivalence-relation?
(make-relation '(0 0) '(1 1) '(2 2) '(3 3))) => #t
(equivalence-relation?
(make-relation '(0 0) '(0 1) '(1 0) '(1 1))) =» #t
(equivalence-relation?
(make-relation '(0 0) '(0 1) '(1 1) '(2 2))) => #f
Managing State
What is change? If we think further about the dining experience of Part I's
introduction, changes took place. Eating left you full, lessened the world's
food supply, enriched the purse of the restaurant's proprietor, and depleted
your buying power. All of these are changes. The state of the world after you
left the restaurant changed. Managing state means that all the effects of a
change must be taken into account.
Part 3 is about combining the management of state with the style of pro-
gramming that we have so far developed. In Chapter 9,we introduce a new
data structure, the vector. A vector is like a list, except that we access it
with operations that use the elements' indices, which are nonnegative inte-
gers, instead of with operations that find the first element and the rest of
9.1 Overview
We have been using lists as our basic data type, most of the ap-
and for
plications we have had so far, lists have been adequate. They do have one
disadvantage that is apparent when we have a long list Is. Let's say it contains
1000 elements, and we want to know what the element with zero-based index
900 is. One way we can access that information is by applying cdr 900 times
and then applying car. That seems like a lot of work, so we use the Scheme
procedure list-ref defined in Program 3.7, which does the cdring for us,
and we invoke (list-ref Is 900). But the computer is doing just as much
work to access the 900th element of the list for us. It would be nice to have a
data type in which we could store elements and look directly into the 900th
(or any other) place and what is there. Being able to access any element
see
in a list using the same amount of computer resources is called random access
into the list. What we now have available to us in lists is sequential access, in
which we have to cdr from the beginning of the list to the desired element. In
this chapter, we study a data type called vectors. Like lists, vectors are used
9.2 Vectors
index 2, is (a, 6, c). The word vector is also used as a data type in Scheme that
associates an element with each integer from zero to some given number. The
elements stored in a vector can be data of any type — for example, numbers,
symbols, lists, or procedures. For the external representation of a vector with
elements a, b, and c, Scheme uses #(a b c). In general Scheme's external
representation of a vector is a sharp symbol, #, followed by the elements
enclosed in parentheses.
As with the other data types we have studied, we begin with certain basic
procedures in terms of which we define the rest of the procedures involving
vectors. The actual representation of the vectors and the basic procedures
will be defined in several ways after we develop the other procedures in a
illustrate the use of this selector, we define a procedure view (See Program 9.1
and Exercise 9.2.) that takes a vector and displays its external representation.
If vec is the vector with the elements 1, 2, 3, and 4, we have
The indices of the elements of the vector vec go through the range from zero
to one less than the length of the vector vec. Thus we locally define highest-
index to be one less than (vector-length vec). The local procedure loop
displays in order the elements of vec, each, except for the last one, followed
by a space. Thus the desired output is obtained by first displaying "#(",
then invoking (loop 0) to display the elements of vec, and finally displaying
")"• In our implementation, we assume that the value returned by a display
expression is suppressed, so the same is true of view.
The constructor vector-generator, which we use for vectors, is a curried
procedure with the call structure
The operand size is a nonnegative integer that is the length of the vector
we are constructing. The generating procedure gen-proc is a procedure that
takes an index as its argument and returns the value to be associated with
(define view
(lambda (vec)
(let ((highest- index (subl (vector-length vec))))
(letrec ((loop (lambda (i)
(display (vector-ref vec i))
(if (< i highest-index)
(begin
(display " ")
that index in the vector we are constructing. The index is any integer in the
range from zero to one less than size. When passed a generating procedure,
vector-generator returns a procedure. When that procedure is passed an
integer specifying the vector's length, it returns a vector having the specified
size and whose elements are determined by the generating procedure. As an
example of the use of vector-generator, we want to construct a vector of
if
Later we shall give ways of representing vectors and defining these basic
procedures. We now show how to define the other procedures that we need,
of the vector we eire building, and the optional second argument is the value
we use to fill the elements of the vector. To accomplish the definition of a
procedure with an optional second argument, we use the unrestricted lambda
in its definition. Thus make-vector has as its parameter a symbol denoted by
surgs. We distinguish between the two cases by testing whether (cdr aurgs) is
empty. we construct a vector of length (car airgs) and fill it with ().
If it is,
Never rely upon this fill value in your programs because the Scheme procedure
make-vector does not specify the fill value if you do not include it as a second
argument. Thus you may be surprised to find the implementation providing
something other than ( ) and your program will not run correctly. If you
,
want to use the fill value in your program, specify it as the second argument
to make-vector, and that value is used to fill the vector that is constructed.
Here is the code for meJce-vector:
(define nake-vector
(lanbda args
(let ((fill-value
(if (singleton-list? args)
'()
(cadr args))))
((vector-generator (lambda (i) fill-value)) (car args)))))
(define li8t->vector
(lanbda (Is)
((vector-generator (laabda (i) (list-ref Is i))) (length Is))))
(define vector
(laabda surgs
(li8t->vector args)))
(define vector-stretch
(lambda (vec new-size)
(let ((size (vector-length vec)))
(let ((gen-proc (lambda (i)
(if (< i size)
(vector-ref vec i)
'()))))
((vector-generator gen-proc) new-size)))))
If the extension of a vector vec is the same size as vec, the extension
Program 9.6.
(define vector-copy
(lambda (vec)
(vector-stretch vec (vector-length vec))))
(define vector-update
(lambda (vec k val)
(let ((gen-proc (lambda (i)
(if (- i k)
val
(vector-ref vec i)))))
((vector-generator gen-proc) (vector-length vec)))))
(define li8t->vector
(lambda (Is)
(let ((vec (ncJce-vector (length Is))))
(letrec
((convert (lambda (Is* v i)
(if (null? Is*)
V
(let ((new-v (vector--update v i (car Is*))))
!
having the same length as the list Is that we are converting into a vector.
Then we form a loop using a letrec expression in which we start with the
vector vec and successively use vector-update to give us a new vector new-v
that has the appropriate element changed to the corresponding element in
the list. This new vector is passed to the local procedure convert, which
continues this process until all of the elements of vec have been updated. Its
definition is given in Program 9.8. We make an improvement in the procedure
list->vector later in this section. At that time, we shall eliminate the need
to pass the vector new-v as an argument to convert and shall produce an
0{n) version instead of (9(n^).
Suppose we have a vector vec and want to construct a new vector whose
elements are obtained by applying the procedure proc to the corresponding
elements of vec. To accomplish this, we use a vector analog of the list proce-
dure map. We call the vector version vector-map, and we define it by:
(define vector-map
(lambda (proc vec)
((vector-generator (lambda (i) (proc (vector-ref vec i))))
(vector-length vec))))
In the language used with vectors, one usually refers to numbers as scalars.
We call a vector a numerical vector if all of its elements are numbers. The
product of a scalar c and a numerical vector (oi, a2,...,an) is the vector
(cai, ca2, . .
.
The procedure multiply— by-scalau: takes as parameters
, ccLn)-
a scalar c and a numerical vector vec and returns their product. It is defined
by
(define multiply-by-scalair
(lambda (c vec)
(vector-map (lambda (elem) (* c elem)) vec)))
(define vector-apply-elementwise-to-both
(lambda (proc)
(lambda (vecl vec2)
(let ((gen-proc
(lambda (i)
(proc (vector-ref vecl i) (vector-ref vec2 i)))))
((vector-generator gen-proc) (vector-length vecl))))))
The sum of two numerical vectors vecl and vec2 of the same length is the
When the index reaches size, there are no more elements to add. We define
vector-sum to be:
(define vector-sum
(lambda (vec)
(let ((size (vector-length vec)))
(letrec
( (helper
(lambda (i)
(if (= i size)
(define vector-product
(lambda (vec)
(let ((size (vector-length vec)))
(letrec
((helper
(lambda (i)
(if (= i size)
1
It should occur to you when looking at the last two definitions that they are
very similar in structure, and that they are ideal candidates for abstraction.
Let's define a procedurevector-accumulate that abstracts the structure of
those two procedures. There are just two essential differences: the value re-
turned when the terminating condition is true, which we call the seed, and
we call proc. Then the defini-
the operator applied in the alternative, which
tion of vector-accumulate is given in Program 9.15. We can now rewrite the
definitions of vector-sum and vector-product using vector-accumulate:
We defined list->vector, but we have not yet defined the Scheme proce-
dure vector->list that produces a list with the same elements as a given
vector. But that is just a recursion on the index with the procedure cons and
the seed (). We can then use vector-accumulate to define vector->list
as shown in Program 9.16. Then
(define vector-accumulate
(Isunbda (proc seed)
(lambda (vec)
(let ((size (vector-length vec)))
(letrec
((helper
(lambda (i)
(if (= i size)
seed
(proc (vector-ref vec i) (helper (addl i)))))))
(helper 0))))))
Suppose that the elements of the vector (15.50, 8.95, 12.00) represent the
price of items we want
buy and the elements of the vector (2, 5, 3) represent
to
the number of each of those items we want. The total amount of money we
spend on the purchases is 2 x $15.50 + 5 x $8.95 + 3 x $12.00 = $111.75.
We found it by taking the products of the corresponding elements in the two
vectors and then summing the products. This type of computation involving
two vectors is used so often that it is given a name. It is called the dot-product
of the two vectors. In general, if u is the numerical vector (ao, ai, On-i) . .
.
,
and V is the numerical vector (&0) &i) •••, ^n-i)) the dot product u • v of
u and V is the number ao^o + t^i&i + • • • + o,n-ibn-i- We already have a
procedure vec* that computes the vector whose elements are the products
same length. We also
of the corresponding elements of two vectors of the
have a procedure vector-sum that sums the components of a vector. The
composition of these two procedures gives us the dot product:
where we use a more general version of compose that allows its second argu-
ment to be a procedure of arbitrarily many arguments. It is defined by
(define compose
(laabda (f g)
(lambda args
(f (apply g args)))))
Although this is the dot product of two vectors, it is not an efficient way of
getting it. In the process of computing the dot product, the first procedure
applied, vec*, constructs a vector containing the products of corresponding
elements of the original two vectors. This is a rather costly and unnecessary
construction, since we can justmake one pass down the elements of the two
vectors and accumulate the sum of the products as they are formed. The
definition of dot-product using this process is
(define dot-product
(lambda (vecl vec2)
(let ((size (vector-length vecl))
(letrec ((loop (lambda (i)
(cond
((= i size) 0)
(else (+ (* (vector-ref vecl i)
(vector-ref vec2 i))
(loop (addl i))))))))
(loop 0)))))
An even more efficient way of computing the dot product of two vectors
uses an accumulator to store the intermediate sums. Its code is given in
Program 9.17.
The cdr of the pair is another pair whose car is the vector's length, and
whose cdr is a procedure. That procedure takes an index as a parameter and
returns the element of the vector with that index. We make the convention
that the indices are zero based. With these conventions, we define vector?
and vector-length, in Program 9.18.
(define vector?
(lambda (arg)
(and (pair? arg) (eq? (car arg) vector-tag))))
(define vector-length
(lambda (vec)
(car (cdr vec))))
(define vector-ref
(lambda (vec i)
((cddr vec) i)))
(define vector-generator
(laaibda (gen-proc)
(laabda (size)
(cons vector-tag (cons size gen-proc)))))
(define vector-ref
(lambda (vec i)
(list-ref (cddr vec) i )))
(define vector-generator
(lambda (gen-proc)
(lambda (size)
(cons vector-tag
(cons size
(letrec
((loop (lambda (i)
(cond
((= i size) '())
(else (cons (gen- proc i)
(loop (addl i.))))))))
(loop 3)))))))
(
gen-proc on each pass and builds a list of the results. We now have
other question raised in the Overview, random access. It is clear that the list
representation does not provide random access to the elements, for we have
to cdr down the list until we find the element with the desired index, and the
computer resources used in this cdring increase with the index. It may appear
as though the representation of the vector using a procedure that assigns an
element to each index gives true random access to the elements, but consider
the following situation where vectors are represented as procedures:
one procedure invocation. Thus, the resources needed to access the different
elements of d depend upon the indices of the elements.
We do not have to give up on random access because Scheme has an imple-
mentation of vectors that does provide it. The third representation we discuss
is that provided by Scheme. The external representation of a vector in Scheme
as a list preceded by a sharp sign, #, is how we have been displaying vectors
with the procedure view. Thus the vector with elements 20, 30, 40, and 50 is
and only the copy has the element with index k changed to c. With the proce-
dure vector-set we invoke (vector-set vec k c) and we do not create
!
, ! ,
a new vector, but instead we change the element with index k in vec to have
the value c. Thus vector-set is not a constructor, since it does not create a
!
an object that becomes the element with index i in vec. The element that
previously had index i in vec is replaced with obj. The value returned by
an invocation of vector-set! is not specified, so do not use the returned
value since it depends upon the implementation of Scheme. Programs that
do use such values are not portable; that is, they cannot be used with other
implementations of Scheme. The purpose of an invocation of vector-set ! is
[3] (vector-set! vl 2 5)
[4] vl
#(0 2 5 6 8)
ignored, since the letrec expression is the first expression in an implicit begin
expression. The value returned by the whole begin expression is the vector
vec. The program makes use of the fact that the vector
structure of this
vec is effects. The mutation is done in the loop within the
changed by side
letrec expression, and when its work is finished, the vector vec is returned.
(define vector-generat or
(lambda (gen-proc)
(lambda (size)
(let ((vec (make--vector size)))
(letrec
((loop (leunbda (i)
(if (< i size)
(begin
(vector-set vec i (gen--proc i))
(loop (addl i)))))))
(loop 0))
vec))))
to set the element with the given index to a new value and then returns the
vector. It is defined in Program 9.22.
(define vector-update!
(lambda (vec i c)
(vector-set! vec i c)
vec))
in Program 9.23.
(define list->vector
(lambda (Is)
(let ((vec (make-vector (length Is))))
(letrec
((convert
(lambda (Is i)
(if (not (null? Is))
(begin
(vector-!set! vec i (car Is))
(convert (cdr Is) (addl i)))))))
(convert Is 0))
vec)))
!
#(2 5 6)
In the first let expression in [2] , the vector a has a certain state that is
say that at any given time the object is in a certain state that is determined by
certain state variables (in our example, they are the elements of the vector).
If we know the state of an object, we know its behavior at that time. When
using mutators, one must be conscious of the fact that each object has a
certain state and that an invocation of a mutation procedure causes a change
in the state of the object. We shall introduce other mutators in Chapter 11
and discuss the changes in state they cause. At this point, we give another
illustration comparing programming in functional style using vector-update
with programming in imperative style using the mutator vector-update !
j (this happens when the length of the vector is even). The resulting vector is
(define swap-maker
(lambda (vec)
(lambda (indexl index2)
(let ((temp (vector-ref vec indexl)))
(vector-update
(vector-update vec indexl (vector-ref vec index2))
index2
temp)))))
again invoked, this time on the vector that was returned. It returns a new
vector that has its element with index index2 changed to temp, completing
the swap.
We illustrate the use of vector-reverse with the following experiment:
«(5 4 3 2 1)
(define vector-reverse!
(launbda (vec)
(let ((sHapv! (swap -maker vec)))
(letrec
((switch (lambda (i j)
(if « i j)
(begin
(swapv! i j)
(switch (addl i) (subl j)))))))
(switch (subl (vector- length vec))))
vec)))
(define swap-maker
(lambda (vec)
(lambda (indexl index2)
(let ((temp (vector-ref vec indexl)))
(vector-update
(vector-update! vec indexl (vector-ref vec index2))
index2
temp)))))
actually makes the changes in the original vector itself so as to change the
state. Such programming with mutations is called imperative programming.
us random access to the elements in the vector. The price we pay is that we
actually change elements in the original vector when we use vector-set!,
so we need it for some reason after the invocation of vector-set
if we !
,
must first make a copy of the original vector using vector-copy and apply
the mutation procedures to the copy. The vector data type has analogs in
other programming languages where they are often called one-dimensional ar-
we state otherwise, we use the Scheme
rays. In the rest of this book, unless
implementation of vectors with the mutator vector-set !
Exercises
first parameter is base, and the procedure that is returned has parameter n.
Test your procedure on
Exercise 9.2
A vector of zero length can be displayed by #(). Rewrite the definition of
view to support vectors of zero length.
Exercise 9.4
Let vl be the vector (vector 12 3 4) and let v2 be (vector-copy vl).
What does each of the following expressions return? Test them with each of
the three representations of vectors.
returns the smallest index whose element is the same as obj. If the object is
not in the vector, an appropriate message is returned. Test your program on:
9.4 Matrices
We have used lists and vectors to store data. In both of these, the elements
are sequentially organized, so that we can index the elements starting with
zero and increasing the index by one for each successive element. We often use
this way of organizing information, but there are also occasions where such
a sequential organization is not the best way of organizing the information.
Many times a table is a more convenient way of presenting data. For example,
in a telephone book, each person has several entries: a name, an address, and
a phone number. The data are entered in rows, each row containing the
information for one person. If we take the first entries in all of the rows, we
get the first column; the second entries of all rows form the second column;
and in general, the nth entries in all rows form the nth column. The table
in Figure 9.28 contains four rows and three columns. Each entry in the table
can be located if we are given two indices, the zero-based index of the row
and the zero-based index of the column. Thus in our table, "3314 Valley Dr."
is the item with row index 2 and column index 1.
A matrix is a table in which each of the entries has two indices, the first
being the zero-based row index and the second being the zero-based column
index. In our discussion, we refer to the element in the matrix A having row
index i and column index j as the element Oij . If the matrix has m rows and n
where nrows and ncols are the number of rows and columns, respectively, of
the matrix we are constructing. The procedure gen-proc takes as its argu-
ments two indices and produces the element with those indices in the matrix
being constructed.
A=
523714058312
This way is called row major order. The other way is to write the columns
one after the other, to get the sequence of elements
518243301752
This way of writing the elements is called column major order. We arbitrarily
choose to use the row major order in our representation of matrices.
Now that we are agreed on row major order, we must have a way of knowing
where one row ends and the next begins. In our example, there are in all 12
(vector 523714058312 4)
(define num-cols
(lambda (mat)
(let ((size (subl (vector- length mat))))
(vector-ref mat size))))
Consider the element in this vector. It has index 6 in the vector. What
indices does it have in the matrix A? Here is how we can compute them using
information we can extract from the vector A. The vector A has length 13,
and the last element is a 4. We use this last element to conclude that the
matrix has rows of length 4. Thus it has four columns. If we remove the
last element, there are 12 elements left, so the matrix has 12/4 = 3 rows.
Now if we start at the beginning of the vector and collect the elements in
groups of four, we see that is in the second group and is the third element in
that group. Thus is in the second row and third column. Using zero-based
indices, we see that has row index 1 and column index 2. We can get these
indices easily by noting that the row index is the quotient when the index 6
is divided by the row length 4, and the column index is the remainder when
6 is divided by 4.
Now let's reverse the process and start with the element in A having row
index 1 and column index 2. We find its index in the vector representing A by
multiplying the row index 1 by 4, which is the number of elements in each row
of A, and then adding the column index 2. Thus the index of that element in
the vector is (1 •
4) -I- 2 = 6.
index j , and it in turn returns the element of mat with those indices. To find
the element in the vector representation of the matrix, we must multiply the
(define nmn-rows
(leunbda (mat)
(let ((size (subl (vector--length mat))))
(/ size (vector--ref mat size)))))
row index i by the number of columns in the matrix mat and add to that
product the column index j. This gives us:
(define matrix-ref
(lambda (mat)
(let ((ncols (num-cols mat)))
(lambda (i j)
(vector-ref mat (+ (* i ncols) j))))))
Exercise
Exercise 9.7
The selector matrix-ref should contain in its definition a range test for the
indices i and j and return an error if either is out of range. Rewrite the
definition so that it contains such a test.
divided by ncols. Recall that ncols is the number of elements in each row
of the matrix. Then to get the row index, we can count the number of groups
of ncols elements we can remove before including the element e with index
k. The number of such groups is the zero-based row index of the element e
in the matrix. But that number of groups is what we mean by the quotient
obtained when k is divided by ncols. The next such group of ncols elements
(lambda (k)
(gen-proc (quotient k ncols) (remainder k ncols)))
The length of the vector representing the matrix is one more than the product
of nrows and ncols. We can then write:
(define matrix-generator
(lambda (gen-proc)
(lambda (nrows ncols)
(let ((size (* nrows ncols)))
(let ((vec-gen-proc
(lambda (k)
(if (< k size)
(gen-proc (quotient k ncols)
(remainder k ncols))
ncols)))
((vector-generator vec-gen-proc)
(addl size)))))))
convenient to curry the procedures row-of and column-of. To get the row of
the matrix mat with zero-based index i, we invoke ((row-of mat) i), and
to get its column with zero-based index j, we invoke ((column-of mat) j).
(define row-of
(lanbda (aat)
(let ((mat-ref (natrix-ref mat))
(munber-of-coluains (nua-cols mat) )
(lambda (i)
(let ((gen-proc (lambda (j) (mat-ref i j))))
((vector-generator gen-proc) number-of -columns)) )))
and
(define column-of
(lambda (mat)
(let ((mat-ref (matrix-ref mat))
(number-of-ross (num-rows mat) )
(lambda (j)
(let ((gen-proc (lambda (i) (mat-ref i j))))
((vector-generator gen-proc) number-of-rows))) )))
columns of A. The transpose of the matrix in Figure 9.29 is the 4x3 matrix
given in Figure 9.36. To we use the procedure
find the transpose of a matrix,
matrix-transpose (see Program 9.37) that takes as its argument a matrix
and returns its transpose. The key to writing the program is the observation
that the element with indices i.j in the transpose of A is the same as the
element with indices j. in .4. It is easily defined using matrix-generator.
i
(define matrix-transpose
(lambda (mat)
(let ((mat-ref (matrix-ref mat))
(let ((gen-proc (lambda (i j) (mat-ref j i))))
((matrix-generator gen-proc)
(num-cols mat)
(num-rows mat ))))))
The elements of matrices can be any of the data types we have been using
in Scheme. For numerical matrices, that is, matrices whose elements are
numbers, we can define useful arithmetic operations. For example, if A and
B are two matrices of the same size (same number of rows and same number
of columns), we can define the sum A + B to he the matrix whose elements
are the sums of the corresponding elements of A and B. Similarly, we can
multiply the matrix A by a, scalar (number) c, and the resulting product cA is
also possible to define a useful multiplication rule for certain pairs of matrices.
It is not customary to define the product to be the matrix whose elements are
the products of corresponding elements of the two matrices. The following
example illustrates the multiplication rule normally used.
A factory produces four products, W, X, Y and Z, at each of two sites, R
and S. Each product uses steel, plastic, and rubber. Product W uses 4 units
of steel, 2 units of plastic, and 2 units of rubber. Product X uses 5 units of
steel, 2 units of plajstic, and 2 units of rubber. Product Y uses 4 units of steel,
3 units of plastic,and 1 unit of rubber. Product Z uses 3 units of steel, 5 units
of plastic, and 2 units of rubber. The cost of the steel, plastic, and rubber at
site R is $8, $4, and $5 per unit, respectively, and at site S is $7, $5, and $4
per unit, respectively. To find the material costs of making one unit of each
product at each of the sites by finding the dot product of the appropriate row
of the Units Used matrix and the appropriate column of the Cost Per Unit
matrix. This leads to the following tabulation of the results:
Site R Site S
w 1 $50 $46
X $58 $53
Y $49 $47
Z V $54 $54
From this example, we make the following observations about the way the
product of two matrices is defined. In order to be able to find the product AB
for two matrices, they must be compatible, which means that the number of
columns of A must be the same as the number of rows of 5. If two matrices
A and B are compatible, then the rule for computing their product AB is: if
(define matrix-product
(Isunbda (mat-a mat-b)
(let ((a-row (row-of mat-a))
(b-col (coliunn-of mat-b)))
(let ((gen-proc
(lambda (i j) (dot-product (a-roH i) (b-col j)))))
((matrix-generator gen-proc)
(num-roBs mat-a) (num-cols mat-b))))))
This way of defining matrix-product follows directly from the rule describ-
ing matrix multiplication, but it is not an efficient way of doing it. Before actu-
ally taking the dot product, the row and column vectors had to be constructed.
However, the answer does not require having these vectors. We can take the
product of two elements at a time and accumulate their sum directly from the
matrices without building the row and column vectors. Each element in the
ith row of A has first index i, and each element in the jth column of B has
second index j. Thus the ith row of A has the elements Oi^Oi cii,i> • • • > Oi,n-i>
(define matrix-product
(lambda (mat-a mat-b)
(let ((ncols-a (niim-cols mat-a))
(a-ref (matrii-ref mat-a))
(b-ref (matrix-ref mat-b)))
(if (not (= ncols-a (num-rows mat-b)))
(error "matrix-product:"
"The matrices are not compatible.")
(let
( (gen-proc
(lambda (i j)
(letrec
((loop
(lambda (r ace)
(if (= r ncols-a)
ace
(loop (addl r)
(+ ace (* (a-ref i r)
(b-ref r j))))))))
(loop 0)))))
((matrix-generator gen-proc)
(num-roHS mat-a) (num-eols mat-b)))))))
where mat is a matrix whose element with indices given by row-index and
(define matrix-set!
(lambda (mat)
(let ((ncols (nvim-cols mat )))
(lambda (i j obj)
(vector-set ! mat (+ (* i ncols) J) obj)))))
Exercises
((matrix 3 4) 5 2 3 7
14 5
8 3 12)
5 2 3 7
14 5
8 3 12
Next, include a tag matrix-tag and generalize the procedure view to dis-
play a vector, set, or matrix depending on the type of its argument. (See
Exercise 9.3)
and k, where f = 0, 1, . .
.
, mi — 1, j = 0, 1, .m2 — 1, and ^ =
.
.
, 0, 1, . .
.
, ma— 1.
We would then represent A as a vector of length mi m2 ma • •
2. The integer
-I-
rrin for n = 1, 2, 3 is the size of the matrix in dimension n and we say that A is
an mi X m2 X ma array. The last two entries in the vector representation can
be taken to be m2 and the product m2m3. Using this information, describe
the vector representationmore completely, and define the procedures size-
dim-1, size-dim-2, and size-dim-S that are the analogs of num-rows and
num-cols and return the values mi, m2, and ma, respectively. Also define the
procedures array-rel, eoray-set! and array-generator, which are analogs
of the corresponding matrix procedures. These considerations can be extended
to give arrays of any dimension.
10.1 Overview
The process of rearranging the data in a list to put them in some specified
order, such as alphabetical order or increasing numerical order, is called sort-
ing the list. The process of locating a given item in a list is called searching
the list for the given item. In this chapter, we develop routines for sorting
and searching both lists and vectors. We also develop a relational calculus for
retrieving from a table those items that satisfy some specified conditions.
10.2 Sorting
We use tables to store many kinds of data, such as names, grades, or salaries.
It is more convenient to access data in tables if the data are arranged in some
increasing or decreasing order. For example, names are most conveniently
arranged in alphabetical order, and test grades are often most conveniently
arranged in decreasing order. There are many methods for sorting a
different
table into a desired order. We shall look at a few of them in this section.
Our first sorting technique is called an insertion sort. Suppose we are given
a nonempty list Is of numbers that are not ordered and that we wish to rear-
range to be in increasing order. We first think about the problem recursively.
If the list of numbers contains only one number, the list is already sorted, and
we are done. If the list contains at least two numbers, and if we recursively
Program 10.1 insertsort
(define insertsort
(lambda (Is)
(if (singleton-list? Is)
Is
(insert (car Is) (ins ertsort (cdr Is))))))
(define insert
(lambda (a Is)
(cond
((null? Is) (cons a '
()))
((< a (car Is)) (cons a Is))
(else (cons (car Is) (insert a (cdr Is)))))))
invoke insertsort on (cdr Is), we get a correctly sorted list containing all
but the first number. To get the completely sorted we have to do is in-
list, all
sert (car Is) into its correct position in the already sorted part (insertsort
(cdr Is)). For this insertion, we afterward define a procedure insert that
inserts a number a into the correct place in a sorted list Is. Program 10.1
contains the definition of insert -sort, and Program 10.2 contains that of
insert.
While (cdr Is) is not empty, insertsort invokes insert to insert (car
Is) into (insertsort (cdr Is)). Until (cdr Is) is empty, the value of
(insertsort (cdr Is)) is not known, so a return table is built. When Is
is finally reduced to contain a single number, then (insertsort (cdr Is))
is just the "sorted" list containing that single number. Then each of the
insertions that has been waiting in the return table can be evaluated, and the
final sorted list is built up. Figure 10.3 shows how the recursive insertion sort
routine sorts a list of numbers. We start with the list (50 40 30 20 10).
When we reach the invocation of insertsort on (10), the sorted sublist
(10) is returned, and the invocations of insert that have been waiting in the
return table are evaluated with sorted sublists as their second arguments.
We also write an iterative instead of recursive version of insertion sort. This
version will be written usingScheme vectors to store the data. We define a
mutation procedure called vector- insertsort that takes as its argument a!
vector containing the numbers to be sorted into ascending order and changes
say with index k, all of the elements with indices less than k have already been
sorted, and vector-insert! inserts the element with index k in the correct
place relative to the first k elements. Now the elements with indices less than
k+ 1 are in the correct order. This insertion process continues until the index
k reaches the length of the original vector, by which time all of the elements
have been inserted. When k is equal to size, the condition in the if expression
the value returned on the screen. The code for vector-insertsort is given !
in Program 10.5.
#(60 50 30 40 10 20)
Insert 50 «(60 30 40 10 20)
#(50 60 30 40 10 20)
Insert 30 #(50 60 40 10 20)
#(50 60 40 10 20)
#( 50 60 40 10 20)
#(30 50 60 40 10 20)
Insert 40 #(30 50 60 10 20)
#(30 50 60 10 20)
#(30 50 60 10 20)
#(30 40 50 60 10 20)
Insert 10 #(30 40 50 60 20)
#(30 40 50 60 20)
#(30 40 50 60 20)
#(30 40 50 60 20)
#( 30 40 50 60 20)
#(10 30 40 50 60 20)
Insert 20 #(10 30 40 50 60 )
#(10 30 40 50 60)
#(10 30 40 50 60)
#(10 30 40 50 60)
#(10 30 40 50 60)
#(10 20 30 40 50 60)
(define vector-insertsort!
(laubda (v)
(let ((size (vector-length v)))
(letrec ((sortloop (lanbda (k)
(if (< k size)
(begin
(vector-insert! k v)
(sortloop (addl k)))))))
(sortloop 1)))))
called with some index m as argument, it compares val with the element comp
having index m— 1. If val is less than comp, we still have not found the correct
place for val, so comp is moved up to have index m, and insert-h is called
again with argument m — 1compare val with the element with index m — 2.
to
On the other hand, if val is not less than comp, m is the correct index for
val, and we assign it with (vector-set! vec m val). Each time insert-h
isinvoked, its argument is one less than on the previous invocation. If its
argument is zero, then there are no more elements to the left to which to
compare it, and val must be the first element in the vector. Thus (zero? m)
is the terminating condition for the recursion. The code for vector-insert
is in Program 10.6.
(define vector-insert 1
(lambda (k vec)
(let ((val (vector--ref vec k)))
(letrec ((insert--h
(leunbda (m)
(if (zero ? m)
(vector-set! vec v&l)
(let ((comp (vector -ref vec (subl m))))
(if (< val comp)
(begin
(vector-set ! vec m comp)
(insert-h (subl m)))
(vector-set 7ec ffl val)))))))
(insert -hk)))))
within that vector, so when we look at v after the sorting, its elements are
(define vector-insertsort
(lambda (vec)
(let ((v (vector-copy vec)))
(vector-insertsort ! v)
v)))
in each case to put the inserted number in the correct place. This was the
case in the example above. But in general, we can say that on the average.
10.2.2 Mergesort
continued until there is only one correctly ordered list, at which point the sort
is completed. In our example, the next merge operation yields ((11222
3 3 4)), and the car of this list is the desired sorted list.
(1 2)). Then successive sorted lists are merged to give ((12 3 4) (12 2
3)) and then ((11222334)). The development of this version of the
mergesort procedure is left as an exercise.
The first step in performing the natural mergesort is to group the data into
sublists so that in each sublist, the data are correctly ordered. In order to
carry out this grouping, we define a procedure maie-groups that takes a list
of numbers, Is, as its argument and returns a list with the numbers in the
same order but arranged in largest possible sublists in which the numbers are
nondecreasing. If Is is empty, then there are no sublists, and () is returned.
This serves as the terminating condition for our recursion. Similarly, if (cdr
Is) is empty, then Is consists of only one element, so there is only one group
consisting of the Is itself. Thus the list of groups is simply (list Is). Now,
if we let gps denote (make-groups (cdx Is)), there are two cases to consider
in order to get (maie-groups Is). If the first number in Is is greater than
the second number in Is, then the first sublist in (make-groups Is) is a
singleton list containing just that first number. Thus we have (cons (list
(car Is)) gps) as the result. On the other hand, if the first number in Is
in not greater than the second number in Is, then it must be added to the
first group alrecidy in gps. Thus the result is (cons (cons (car Is) (cau:
Program 10.7.
(define pair-nerge
(lambda (sublists)
(cond
((null? sublists) '())
((null? (cdr sublists)) sublists)
(else (cons (merge (car sublists) (cadr sublists))
(pair-merge (cddr sublists)))))))
the last sublist of the list returned by pair-merge. Thus the procedure pair-
merge has as its parameter suolists, which is a list of sublists, and in each
sublist the numbers are in ascending order. It returns a list in which each
successive pair of sublists has been merged. Its straightforward definition
is given in Program 10.8.
(define nat-mergesort
(lambda (Is)
(if (null? ]-S)
'()
To see that the nat-mergesort procedure is 0{n log n), we consider a worst
What we have asked is: "What is the smallest power k for which 2*^ > n?"
Again if 7,n = and 2^ = 8, so the value of k is 3. But the
then 2^ = 4
statement 2^ = 8 means exactly the same thing as the statement log2 8 = 3,
and in general, the statement 2* = n means exactly the same thing as the
statement logj n = Thus the number of passes through the list to get
k. to the
sorted list is at most log2 n rounded up to the next higher integer if it is not
a whole number. We can use the ceiling procedure to do this rounding and
use the usual notation [logj n\ . In merging two lists that together contain
m numbers, at most m— I comparisons are needed, so if all of the sublists
together contain n numbers, the number of comparisons needed in any one
pass of sort through the list we make at most
will never exceed n. Since
|log2 n] passes through the list and each pass needs at most n comparisons,
we have a total of at most npogjn] comparisons. Thus nat-mergesort is
0(n log n).
There are also situations in which we want to sort a list of numbers into
decreasing order. The sorting procedures given can easily be modified to do
this; for example, in nat-mergesort, one only has to change the < to > in the
two procedures merge and make-groups. It would be even more convenient
to use procedural abstraction here and write one nat-mergesort procedure
that takes an additional argument rel that will be either < or >, and replace
the inequalities in merge and maie-groups by rel.
An iterative vector version of mergesort can also be defined. Since we shall
be sorting data stored in vectors, we look at such a version. A recursive
vector version of mergesort is outlined in the exercises. The iterative sorting
program has three parts. We first determine the group size, with initial value
1. From the group size, we determine the end points of the groups. Then
we merge successive pairs of groups, with the usual proviso that if there is
an odd number of groups, one is left over after the merging. After the merge
phase, we double the group size and repeat the process. This continues until
(define vector-merge!
(launbda (newvec vec)
(lambda (left top-left right top-r ight)
(letrec
((mergeloop
(lambda (left right i)
(cond
((and (< left top-left) (< right top -right))
(if (< (vector-ref vec left) (vector-ref vec right))
(begin
(vector-set! newvec i (vectoi-ref vec left))
(mergeloop (addl left) right (addl i)))
(begin
(vector-set! newvec i 'vectoi—ref vec right))
(mergeloop left (addl right) (addl i)))))
]
the group size is the same as the length of the vector, at which time the sort
is completed. Since the initial group size is 1 for all groups, this is an ordinary
rather than a natural mergesort.
in Program 10.10.
We now carry out the sort using the procedure vector-mergesort !, given
in Program 10.11, which has a vector vecl to be sorted as its parameter. Since
the mutator vector-merge I merges two adjacent groups of one vector into
another vector, we must always have another vector into which to carry out
(define vector-nergesort
(lanbda (vecl)
(let ((vec-size (vector-length vecl))
(let ((adjust (lambda (k) (min k vec-size)))
(vec2 (meike-vector vec-size))
(max-index (subl vec-size)))
(letrec
( (merge-pass
(lambda (group-size count)
(if (> group-size maix-index)
(if (even? count) (vector-change! vecl mzix-index vec2))
(let ((newvec (if (odd? count) vec2 vecl))
(vec (if (odd? covuit) vecl vec2)))
(let ((merge! (vector-merge! newvec vec)))
(letrec
((group-ends
(lambda (left top-left right top-right)
(if (<= left meuc-index)
(begin
(merge! left top-left right top-right)
(let ((new-right (+ top-right group-size)))
(group-ends
top-right
(adjust new-right)
new-right
(adjust (+ new-right group-size)))))))))
(group-ends (adjust group-size)
group-size (adjust (* 2 group-size)))))
(merge-petss (* group-size 2) (addl coimt) ))))))
(merge-pass 1 1))))))
this merge. Rather than create a new vector on each merging pass through
the vector, we make a second vector vec2 and alternate merges from vecl
into vec2 and from vec2 into vecl on successive merging passes.
On the first pass through the vector, we assume that each group contains
only one item, so the parameter group-size to the local procedure merge-
pass is given the initial value 1, and its second parameter count is set to 1.
On each successive merging pass through the vector, two adjacent groups are
(define vector-change!
(lambda (vecl j k vec2)
(letrec ((loop (leunbda (i)
(if (<= i k)
(begin
(vector-set I vecl i (vector-ref vec2 i))
(loop (addl i)))))))
(loop j))))
The mutator vector-change! copies the segment of vec2 with indices be-
tween j and k into vecl, actually changing the items in vecl. This vector
version of mergesort sorts a vector in just about the same time as the list
version of mergesort sorts a list of the same size. It is somewhat slower than
natural mergesort, but it has the advantage of not requiring a return table
10.2.3 Quicksort
done by mutation within the original vector, and no temporary storage vector
is necessary.
1. We first select an item in the list, which we call the pivot. For convenience,
we shall select the first item in the list, although selecting other items as
the pivot sometimes improves the performance of the method.
2. The rest of the items in the list are copied into one of two lists. Those less
than the pivot are copied into the left list, and the rest are copied into the
right list. The order of the items in these two lists is immaterial.
3. A new list is created in which the items in the left group are followed by
the pivot, which is, in turn, followed by the items in the right group.
(define quicksort
(letrec
((collect
(lambda (pivot Is Igroup rgroup)
(if (null? Is)
(append (qui cksort Igroup) [cons pivot (quicksort rgroup)))
(if (< (car Is) pivot)
(collect pivot (cdr Is) (cons (car Is) Igroup] rgroup) )
the pivot. If it is, it conses it onto Igroup; otherwise it conses it onto rgroup.
When Is is finally empty, the pivot is consed onto the quicksort of rgroup,
which is then appended to quicksort of Igroup. The definition of quicksort
is given in Program 10.13.
We also present the vector version of quicksort, which we call vector-
quicksort ! . It has the property that it sorts the data within the given
vector without having to make a copy of it, as mergesort must do. This is an
advantage if theamount of memory available is small and making a copy uses
too much of the memory. Like vector-mergesort !, vector-quicksort! is
a mutator that changes the order of the elements in the original vector.
in place (that is, within the given vector), we shall walk through the sorting
of the vector
#(649782569)
To see how this version of quicksort works, we place our left index finger below
the item to the right of the pivot (the item with index 1) and our right index
finger below the last item in the vector (the item with index 8 in our example).
This is illustrated by the arrows under the items:
#(649782569)
/ \
position, so that all items to its left will be less than or equal to the pivot eind
all items to its right will be greater than or equal to the pivot.
1. Searching Up. We compare the item above our left index finger with the
pivot and if it is not greater than the pivot, we move our left index finger
one entry to the right and compare that item with the pivot. We continue
moving to the right until we encounter an item that is greater than the
pivot. We then hold our left index finger at that position. If we pass
beyond the last item, we go back to it. In our case, we have moved to the
item with index 2, and the positions are given by:
#(649782569)
/ \
2. Searching Down. We next compare the item above our right index finger
with the pivot and if it is not less than the pivot, we move our right index
finger one entry to the left and compare that item with the pivot. We
continue moving to the left until we encounter an item that is less than the
pivot. We hold our right index finger at that position. we come to the If
first item, we stop there. In our example, we move down until we encounter
the item 5 with index 6.
#(649782569)
/ \
3. Swapping. Swapping allows us to move both values from the wrong location
to the right location. As long as our left index finger is to the left of our right
index finger, we swap the items to which our index fingers are pointing,
and we move our left index finger one item to the right and our right index
finger one item to the left. This gives us:
#(645782969)
/ \
We repeat steps 1, 2, and 3 starting with our index fingers pointing to the
7 and 2 and using the same pivot, 6. Since our left index finger is pointing
to the item 7, which is greater than the pivot, and our right index finger is
pointing to the item 2, which is less than the pivot, we swap those two items
and move our index fingers to the next item, our left index finger moving right
and our right index finger moving left. This gives:
#(645287969)
pointing to 8, which is greater than the pivot, it stays where it is. Our right
index finger is pointing to 8, which is greater than the pivot, so it moves one
to the left and comes to 2, which is less than the pivot, so it stays there. We
now have:
#(645287969)
X
4. Partition. After searching up and searching down, our left index finger is
pointing to an item that is to the right of the item to which our right index
finger is pointing. When this happens, we swap the pivot item with the
item to which our right index finger is pointing. This gives us:
#(245687969)
We next partition the vector into three parts: the left part consists of all items
to the left of the pivot 6 (these items are all less than 6); the pivot itself makes
up the middle part; and the right part consists of all items to the right of the
pivot (these items are all greater than or equal to 6).
#(245687969)
We now apply Steps 1 through 4 to the left and right parts, choosing the
leftmost item in each part as the pivot. We continue doing this until each
part contains only one point. When we reach that point, the vector is sorted.
done by the helping procedure qsort, which has as parameters the lowest
and highest indices of the part of the vector to be sorted. As long as there is
—
more than one item in that part that is, the lowest index, low, is less than
—
the highest index, high the helping procedure partition (defined later) is
called to carry out Steps 1 though 4, and the final position of the pivot is
called middle. Then the qsort procedure is invoked on both the left part
and the right part.
The procedure partition selects the pivot as the first item in the group
to be partitioned and then calls the local procedure search, which in turn
invokes the local procedures search-up and search-down to carry out Steps
1 and 2 given above. The indices of the items that search-up and search-
less than new-right, the items with these indices are swapped, using the pro-
cedure vector-swap! defined in Program 10.16, and the searching continues
as described in Step 3. Otherwise, the pivot is swapped with the item with
the index new-right, and new-right is returned as the value of paortition.
The definition of partition is given in Program 10.15.
To get an idea of why quicksort is 0{n log2 n) for some lists, let's assume
that the items to be sorted are well mixed so that the correct location of the
pivot is near the middle of the list each time a left group or a right group is
sorted. Let's also assume that the list contains n = 2"* items; then m = logj n.
On the first pass, there will be approximately n comparisons with the pivot
(actually n — 1). If the correct location of the pivot item is near the middle
of the list, the group and the right group each contain approximately ^
left
comparisons. Again assuming that the correct location of each pivot is near
the middle of the group, we have four groups each, containing approximately
empty and the right group contains n— 1 items. Thus it takes n— 1 comparisons
(define partition
(laabda (v low high)
(let ((pivot (vector-ref v low)))
(letrec
((search
(laabda (left right)
(letrec
((search-up
(lambda (i)
(cond
((= i (addl right)) (subl i))
((> (vector-ref v i) pivot) i)
(else (search-up (addl i))))))
(search-down
(lambda (i)
(cond
((or (= i (subl left)) (< (vector-ref v i) pivot)) i)
(else (search-down (subl i)))))))
(let ((new-left (search-up left)
(new-right (search-downright)))
(if (< new-left new-right)
(begin
(vector-swap! v new-left new-right)
(search (addl new-left) ^subl new-right)))
(begin
(vector-swap! v low new-right)
new-right)))))))
(search (addl low) high)));)
(define vector-swap!
(lambda (vec i j)
(let ((temp (vector-ref vec i)))
(vector-set! vec i (vector-ref vec j))
(vector-set! vec j temp))))
group and a right group containing n — 2 items. Continuing this way, we see
n + (n - 1) + (n - 2) + •
+ 2 = -(n^ + n - 2)
comparisons, which is Oin"^). Thus quicksort has the strange property that
it is the most efficient method for sorting data that are completely unsorted
and least efficient for data that are nearly sorted.
Often the data to be sorted consist of names that one would like to have
rearranged in alphabetical order. Scheme provides a convenient way to do
alphabetical sorts. Strings can be compared to decide which precedes the other
in alphabetical order. To decide whether one string precedes another string,
Scheme provides a predicate string<? that takes two strings as arguments
and is true if its first argument precedes its second argument lexicographically
(i.e., in the order in which they would appear in a dictionary). The usual
lexicographic order is modified to place the digits through 9 first, then all of
the capital letters, followed by all of the lowercase letters. A complete listing
There are also predicates string>?, string<=?, and string>=? that do the
expected things. The predicates string<? and string>? can be used in place
of < and > in the sorting programs to sort names into alphabetical order. If
the sort routine had been abstracted to take an additional argument rel, then
the alphabetical sort could have been accomplished by passing rel the value
string<? or string>?.
The data being sorted are sometimes in a table that can be sorted in many
different ways depending upon which column of the table we want in sorted
order. For example, a table may contain personnel data for a company. Each
row of the table contains the data for an individual employee. In the study of
databases, each row of the table is referred to as a record. The first column
contains the name, with last name first. The second column contains the
employee's identification number; the third column, the employee's age; the
(define tablelO-17
'(("Smith, Harold W." 2324 43 1974 "Fox, Charles Q." 49325)
("Jones, Mary Ann" 1888 54 1965 "none" 65230)
("White, Thomas P." 3403 34 1982 "Smith, Harold W." 27300)
("Williams, John" 2451 46 1970 "Jones, John Paul" 41050)
("Brown, Susan E." 3620 28 1984 "Williams, John" 18500)
("August, Elizabeth" 2221 45 1971 "Jones, John Paul" 44100)
("Jones, John Paul" 1990 55 1965 "Jones, Maury Ann" 63700)
("Wilson, William W." 2455 46 1970 "August, Elizabeth" 41050)
("Black, Burton P." 3195 38 1978 "Smith, Harold W." 31420)
("Fox, Charles q." 2400 41 1981 "Jones, John Paul" 52200)
("Blue, Benjamin J." 3630 26 1984 "Williams, John" 18500)))
fourth column, the year when the employee joined the company; the fifth col-
umn, the employee's supervisor; and the sixth column, the employee's annual
salary in dollars. Each of these columns is called a field, and each field has a
field name. The third field has the field name "Age." To process the data in
Scheme, one can represent it as a list (or in some cases, as a vector or a list of
To sort the data in this table, we must first decide which field we are going
to sort on; that field is referred to as the sort key or simply the key. We can
sort these data so that the names are in alphabetical order, in which case we
are sorting on the first field, or we are sorting with the key "Name." One
could also sort the data by any of the other fields, depending upon the use
we are making of the data. The sorted data are then returned with the same
records but reordered so that the sorted field is in the desired order. We must
(or rel
modify the less-than predicate in the abstracted version) of the sort
row and then moves the whole row as a unit as the sorting is done.
is between and n — 1.
(define random-list
(lambda (n)
(letrec ((build-list
(lambda (k)
(if (zero? k)
'0
(cons (random n) (build-list (subl k)))))))
(build-list n))))
We can measure the time that each of the three sorting procedures takes to
sort these lists (or vectors) using the procedure timer, which we now define.
When we pass timer a procedure proc and its argument arg, it tells us both
the time in seconds that it took to run (proc arg) and the answer returned
by (proc arg). In order to do the timing, we assume that the Scheme we
are using has a way of getting the time of day accurate to hundredths of a
second. We assume that we can access the time of day by calling a procedure
^
{xfc}, k = 0, 1,
Such sequences of pseudo-random nvunbers can be generated in the
. . .
following way. Consider a congruence relation of the form /(x) = ax mod m, where is m
a large prime integer and a is an integer in the range 2, 3, . . . , m
— 1. Good choices for
m and a are m= 2^^ - 1 = 2,147,483,647 and a - 7^ - 16,807. We choose an initial
seed si and generate the sequence s^^i = /(*fc)> ^ = ^>^ Then u^ = si^/m is a
pseudo-random number in the range < u^ < 1, and a pseudo-rcmdom integer x^ in the
range < x^ < n — 1 is the integer part of nuh. (See Park and Miller 1988.)
(define timer
(lambda (proc arg)
(let ((start (time-of-day)))
(let ((val (proc arg)))
(let ((finish (time-of-day)))
(let ((elapsed-time (/ (- finish start) 100)))
(writeln "Time = " elapsed-time ", Answer = " val)))))))
^ Although the way of getting the time of day has not been standardized, eill Schemes have
some way of either getting the time of day or of timing a procedure.
The pair bindings in a let* expression are evaluated from left to right, and
any ua/-part may contain var's from previous pair bindings. This enables us
to define timer in the following way:
(define timer
(lambda (proc arg)
(let* ((start (time-of-day))
(val (proc eirg))
(finish (time-of-day))
(elapsed-time (/ (- finish start) 100)))
(writeln "Time = " elapsed-time ", Answer = " val))))
The results of using timer on the various we have devel- sort routines
oped are given in Table 10.20."^ The three mutators vector-insertsort!,
vector-mergesort!, and vector-quicksort! were called by first making a
local copy of the vector to be sorted and then calling the timer on the invo-
cation of the mutator. For example, for vector-insertsort ! we have
The time for the two insert sorts increases approximately by a factor of four
each time the length of the data is doubled, whereas mergesort's and quick-
sort's time increases by a factor between two and three.
The test was conducted in PC-Scheme (Version 3.02) on a Zenith 386 microcomputer.
We have seen the behavior of the various sorting routines on a list of ran-
domly distributed integers. It is also interesting to compare their behavior
on lists that are nearly sorted. Suppose we have a list starting with 50 and
followed by the integers from 1 to 100 in their natural order. We want to sort
this list into increasing order. This is equivalent to saying we want to insert
50 into the sorted list of integers from 1 to 100. We shall test our six sorting
routines on this problem of inserting 50 into the integers from 1 to 100, as
well as the problems of inserting 100 into the integers from 1 to 200 and of
inserting 200 into the integers from 1 to 400. The vector versions are tested
on the vectors obtained by converting the lists to vectors. The results are
shown in Table 10.21.
In this kind of insertion into a sorted list, insertion sort and natural merge-
sort are about equally fast, and quicksort is much slower. We cannot recom-
mend any one of these routines as being the best. We must choose the sorting
routine that is best suited to do the job we have in mind for it. There are
many other sorting routines that have their advantages for specific kinds of
sorting jobs. However, we shall not explore this subject any further in this
book. There is an extensive literature on this topic in which you can read
about other ways of sorting data.
Exercises
as its argument. It returns the list sorted so that successive pairs of elements
satisfy the binary relation. For example, if the list contains numbers, to sort
the list in ascending order we use the procedure (nat-mergesort <), and
to sort a list of names in lexicographic order we use the procedure (nat-
mergesort string<?). Test your procedure on suitable lists of numerical
data and lexicographic data.
Exercise 10.3
Modify the mergesort program so that it can sort Table 10.17 by any one of
the six fields, and in the case of numerical fields, the sort can be in either
increasing or decreasing order. Test your program by sorting the data into
four separate tables, the first sorted alphabetically by Name, the second into
increasing order by Id, the third by decreasing order of Salary, and the fourth
by increasing Date of Employment.
below.
a. Make a vector, store, that has the same length as the vector vec to be
sorted.
b. Define a local procedure sort that takes as its parameters first and last,
which are the indices of the first and last items, respectively, in the group
within vec to be sorted in a given recursive pass. The initial values of
first and last are and one less than the length of vec, respectively.
halfway between first and last. Then in a begin expression (which may
be implicit), do Steps d through g.
g. Copy the part of store between first and last into vec.
Compare the times for this version of mergesort to sort the various test vectors
with the times for the iterative vector version and with the times for natural
mergesort and ordinary mergesort to sort the test lists.
10.3 Searching
information in the row containing the item. We can easily write a program
that searches through the list from the beginning until the given item is found,
and if it reaches the end of the list without finding it, the program indicates
that the element is not in the list. A vector version of this procedure is in
If the list is already sorted into increasing (or decreasing) order, there is a
more efficient method for searching for a given item, which we call the target.
This more efficient method, called binary search, is described next. There is
A can answer only "yes" or "no." The object is to determine the target by
asking the least number of questions. The most efficient strategy for asking
the questions is to divide the interval in which the target must lie into two
equal parts and question whether the target lies in one of these parts. For
example, the first question could be: "Is the number greater than 50?" If the
answer is yes, the next question should be: "Isthe number greater than 75?"
By halving the length of the interval in which the target can lie with each
less than the target, we know that the target lies in the right half of the vector,
so we repeat the search procedure in the right heJf of the vector. If the item
with index n/2 is greater than the target, we know that the target lies in the
left half of the vector, and we repeat the search procedure in the left half of
the vector. This procedure divides the length of the vector by two every time
it is repeated, so it takes at most k steps to reach the point where the length is
reduced to one and the item is found, where 2* = n; that is, fc = logj n. Thus
a vector of 1,000 items that would take an average of 500 comparisons to find
an item using linear search would take at most 10 comparisons to find the
item using binary search.* This points out the advantage of using a method
that is O(logn) instead of 0(n).
Since we shall want to use the bineo-y search program to search for items
in vectors that may either be sorted in increasing or in decreasing order, or
in fact, in vectors of names that are sorted lexicographically, we shall write
[1] (let ((names (vector "Ann S" "Ben J" "Ed A" "Guy S" "Kay ¥")))
((binary-search string<?) names "Guy S"))
3
(define binary-seeurch
(lambda (rel)
(lambda (vec target)
(letrec
((search
(leunbda (left right)
(if (< right left)
(writeln "The search failed.")
(let ((middle (floor (/ (+ left right) 2))))
(let ((mid-val (vector-ref vec middle)))
(cond
((rel tEurget mid-val)
(search left (subl middle)))
((rel mid-val target)
(search (addl middle) right))
(else middle))))))))
(search (subl (vector-length vec)))))))
[2] (let ((names (vector "Ann S" "Ben J" "Ed A" "Guy S" "Kay W")))
(alpha-search names "Ed A"))
2
Exercises
Exercise 10.6
Write the procedures that are used to sort the personnel data in Table 10.17
on the field of names, and then use binary search to locate a given name and
return the personnel data for that individual. Use vectors to store the data
and use the vector sort routine you wish.
We have seen how information is stored in tables. Table 10.17, which contains
personnel data for a company, is an example of such a table. In accessing
information stored in a table, we often want to get all items that satisfy
certain conditions. For example, in Table 10.17 we can ask for the personnel
data for all people who joined the company we can ask
after 1980. Similarly,
for all people over 40 years of age whose salaries are over $50,000. We now
develop a relational calculus that will enable us to access data in this way
using the quantifiers f or-all and there-exists and the procedures set-
builder and set-map defined in Chapter 8. We assume in this section that
the data are stored in lists.
is equivalent to
In the tables in which we store our data, we do not want to have two rows
exactly the same. For that reason, we take, as the data structure to represent
the table, a set whose elements are lists, each of which is one row of the table.
We use the set procedures to manipulate the information stored in the table.
For example, if we want to use the information in Table 10.17, we convert it
to a set using
(define iinlist
(lambda (proc)
(lambda (Is)
(apply proc Is))))
of age. We write the predicate age-test?, which has as arguments the field
names in our table and tests to see if the age entry is over 25. We shall, in
(define age-test?
(unlist
(lambda (name id age yr-emp supervisor salciry)
(> age 25))))
Then we apply the procedure for-all to set 10-17 with the predicate age-
test? in the following way:
It returns true since all of the employees are over 25 years of age. Had we
replaced the 25 by 35 in age-test?, for-all would have returned false, since
some of the employees are not over 35.
In a similar wav we can use the procedure there-exists. For example, if
we want to know if anyone under 50 years of age receives a salary over $50,000,
we write the predicate age<50&salary>50000? as follows:
(define age<50&salciry>50000?
(unlist
(lambda (neune id age yr-emp supervisor salciry)
(and (< age 50) (> salary 50000)))))
((for-all
(unlist
(lambda (n i a y s p)
(or (8triiig<? n s)
((there-exists
(unlist
(lambda (n* i» a* y* s* p*)
(and (8tring<? n* n) (string<? s* s)))))
8etlO-17)))))
setlO-17)
The parameters for the inner lambda expression had to be chosen to be vari-
ables different from those of the first lambda, for if we had used n cind s as
This would always be false and is certednly not what we weuit. Although the
names selected for parameters in a lambda expression are generally arbitrary,
it is necessary to watch for name conflicts in combining such procedures as
lor-all and there-exists. (Incidentally, the expression above returns false
since the conditions are not met by the employee Wilson whose supervisor is
August.)
The two procedures for-all and there-exists return true or false de-
pending on the predicate and the set constructed from the data table. We
shall refer to that set as the table, even though it is a set. To obtain the
names and ages of cill persons in Table 10.17. we write
(define nametage
(unlist
(laimbda (name id age yr-emp superYisor salary)
(list name age))))
we get a table of all of the names in Table 10.17 followed by the field of ages.
The other fields have not been included. We call this new table the projection
of Table 10.17 onto the name and age fields. In a similar way, we can obtain
projections of Table 10.17 onto any of its fields.
Sometimes we want to get the actual rows of the table for which some
predicate is true. In particular, suppose we want to get the data from the
table about all people who are over 45 years of age. The data we want for
each one are the Id, the Age, the Year Employed, and the Salary. We write a
predicate over-45? as follows:
(define over-45?
(unlist
(lambda (name id age yr-emp supervisor salary)
(> age 45))))
(define over45-set
((set-builder over-45? the-empty-set) setlO-17))
To get the desired output, namely, the Id, Age, Year Employed, and Salary,
for each of the selected employees, we project the set over45-set onto the
four desired fields using set-map and then pass the resulting set to set->list
to construct the new table.
(set->list
(set -map
(unlist
(lambda (name id age yr-emp supervisor salary)
(list id age yr-emp salary)))
over45-set))
=> ((1888 54 1965 65230)
(2451 46 1970 41050)
(1990 55 1965 63700)
(2455 46 1970 41050))
The procedure set-map builds a new set consisting of parts of the rows of
the given table, then the list representation of the set contains no repetitions.
In our previous example, had we asked for the retrieved data to be the age.
Jones, J.P.
Blue Wilson
White Black
year employed, and the salary, we would have had two rows like (46 1970
41050), but set-map would have included only one of them in the set. To
avoid such loss of information, at least one of the fields over which you project
should identify each individual uniquely.
In Table 10.17, each person has an immediate supervisor. The organiza-
tional chart for the company is given in Figure 10.24. White is under imme-
diate supervision of Smith but remotely under Fox, J. P. Jones, and M. A,
Jones. Blue is under the immediate supervision of Williams, but remotely
under J. P. Jones and M. A. Jones. We can ask the question: "Who is the
closest common supervisorfor both White and Blue?" Going up the tree from
these two,we see that their closest common supervisor is J. P. Jones. We shall
write a program to find the closest common supervisor of two members in a
supervision tree determined by a table such as Table 10.17. We make the
eissumption that each person has at most one immediate supervisor and that
the tree is connected so that there is one person at the very top.
The program will first start at one of the two members and build the path
up the chart until it goes as high as it can. The path will be a list consisting
of the names starting with one member, say. White, and containing (in order)
all of the names on the path until the top, M. A. Jones. To build this path, we
(define find-supervisor
(unlist
(lambda (name id age yr-emp supervisor salcury)
(lambda (v) (if (string=? name v) supervisor «f)))))
need the procedure build-path, which takes a name like Smith in the table,
determines his supervisor (Fox), and adds Smith to the path, then determines
Fox's supervisor, and so on. When a person finally has no supervisor, that
person is added to the path, and the path terminates. The procedure that
determines a person's supervisor is called find-supervisor. For Table 10.17,
its definition is given in Program 10.25. When given the entries in a row of
the table, find-supervisor returns a procedure that takes the name of a
person, and if that person's name is in the given line, it returns that person's
supervisor; otherwise it returns false.
The strategy we use to find the closest common supervisor for two people,
say X and y, once their two path-lists to the top have been found is to start at
the rear of each path-list, where the two path-lists have the same person (in
our case, M. A. Jones) and move simultaneously toward the front in both lists
until the corresponding elements in the two lists differ. The preceding name
(the last one for which the two lists agree) is the closest common supervisor.
In Figure 10.24, the two paths to White and Blue split apart at J. P. Jones, so
he is their closest common supervisor. It is easier to cdr down a list from the
front to the back, so we reverse each of the two path-lists found for x and y,
so that they now agree in their first elements. The procedure f ind-ccs then
cdr's down the lists comparing their first elements. When these first disagree,
the previous one is returned as the closest common supervisor. The code
for the procedure closest-coinmon-supervisor Program 10.26. is given in
When reading the code for closest-conunon-supervisor, keep in mind that
when closest-coinmon-supervisor is applied below, the parameter test-
procedure is bound to find-supervisor. Since the tables we use are sets of
lists, we use the set operations pick and residue defined in Chapter 8 as the
In this chapter, we have demonstrated how data are handled using lists,
vectors, and sets. We chose the data structure for storing the data that was
most convenient for the application we had in mind. For a binary search, a
vector was used. For sorting, vectors or lists were used to store the data. For
using the relational calculus to search in a table, we found sets of lists to be
a convenient data structure. We have so far developed these important data
types to store data. In later chapters, we shall introduce several more useful
data types, such as the one called objects with which we shall implement
stacks and queues (Chapter 12) and the one called streams (Chapter 15).
Exercises
( (set-builder-map
over-45?
(unlist
(lambda (name id age yx-emp supervisor salary)
(list id age yr-emp salairy)))
the-empty-set
setlO-17)
Exercise 10.8
Write a procedure that accesses the data in Table 10.17 and returns the names
and numbers of all employees who are over 40 years of age, were
identification
hired before 1975, and whose salaries are over $43,000.
Exercise 10.9
Table 10.27 contains the monthly sales for January and February for the
employees listed according to their identification numbers, arranged in in-
creasing order. These are the same employees whose personnel data are given
in Table 10.17. The owner of the company wants a table that contains the
employees' names in alphabetical order and their January sales. Modify the
sorting, searching, and relational procedures given in this chapter to produce
Id Jan-Sales Feb-Sales
(define table 10-27 '( (1888 22300 33000)
(1990 61080 49320)
(2221 41000 52200)
(2324 25550 31500)
(2400 31010 25250)
(2451 28800 16500)
(2455 72050 50010)
(3195 60500 40220)
(3403 31100 22500)
(3620 31100 22500)
(3630 26300 19400)))
Exercise 10.10
The procedure closest-common-supervisor defined in Program 10.26 com-
putes the paths using the local procedure build-path, which recursively pro-
duces the path-list. This path-list is later reversed when it is used as an ar-
gument to f ind-ccs. Rewrite the definition of closest-common-supervisor
so that build-path also has an accumulator as an argument and builds the
list iteratively. The final value of the accumulator is then the desired list in
the order from the top of the tree to the bottom, and reversing it is no longer
necessary when it is used as an argument to f ind-ccs.
Redo the examples of Section 10.4 using this representation. What are its
advantages and disadvantages?
11 Mutation
11.1 Overview
and Scheme evaluates val and binds that value to var. Scheme does not
specify what a set! expression returns, so the value returned is implementation
dependent. In this book, we suppress the value returned by the invocation of
.
a set! expression and write the next prompt. The use of set ! is illustrated in
the following:
[4] (f 5)
50
[5] (let ((f (lambda (x) (+ x 100))))
(writeln (f 5))
(set! f (lambda (x) (* x 100)))
(f 5))
105
500
[6] (f 5)
50
In this example, the define expression is used to bind f to the procedure that
adds 10 to its argument. In [2], f applied to 5 returns 15. In [3], a set!
written to the screen and produces 105. Then a set! expression is used to
rebind f to the procedure that multiplies its argument by 100. Observe that
this rebinding affects only the local binding of the variable f . Then the value
of (f 5) is returned, producing the value 500. In [6] , f is again applied to 5,
and the global binding of f is used to give the value 50. The set! expression
within the let expression affected only the local binding of f
We may think of the set! expression looking up the variable var in the
relevant environment (local or global, determined by the lexical scoping) and
actually replacing the value to which var is bound with the value of val.
342 Mutatton
with an operator called push! that takes the object as its argument. The last
object put into the stack is removed by the operator pop It is characteristic! .
of stacks that the last object that was entered is the first object removed.
This property is described with the name LIFO, which stands for "last in,
first out." Thus, if we push the numbers 10, 20, and 30 onto the stack in the
given order, on the first pop, 30 is removed, on the second pop, 20 is removed,
and on the third pop, 10 is removed.
We now look at one way of implementing stacks (we shall return to the
subject again in Chapter 12 for a better implementation). For our present
purposes, we implement a stack as a list of objects. The car of the list will
be the top of the stack. We next describe several procedures that perform
operations on a stack. The procedure empty? tests whether the stack is the
empty list. The procedure top returns the top of the stack, that is, the car
of the stack. The procedure push adds its argument to the top of the stack.
!
The procedure pop! removes the top of the stack. Finally, the procedure
print-stack prints the whole stack. The definitions of these procedures are
given in Program 11.1.
We have elected to print a stack with the word TOP preceding it to show
:
which end is the top of the stack. Thus the stack containing the numbers 1,
2, 3, and 4, with 1 on top will be printed as
TOP: 12 3 4
assume that we call this procedure often with various arguments. It would
save time if we could store each new value that the procedure computes in a
table, and each time the procedure is called, the table is first checked to see if
the procedure has already been invoked with that argument. Then the already
(define empty?
(lambda ()
(null? stk)))
(define top
(lambda ()
(if (empty?)
(error "top: The stack is empty.")
(car stk))))
(define print-stack
(lambda ()
(define push!
(lambda (a)
(set! stk (cons a stk))))
(define pop!
(lambda ()
(if (empty?)
(error "pop!: The stack is empty.")
(set! stk (cdr stk)))))
computed value is returned rather than recomputing it. If it has not already
been computed, then it is computed, and the new value is both returned and
entered into the table. Since we are assuming that the computation of proc
is a relatively long process, it is more efficient to look the value up in the
table rather than recompute it. This process of making a table of values that
have already been computed and searching through this table each time the
procedure is called to see whether the value has already been computed for
The data structure that we use to serve as a table in which to store the
already-computed values is a list, which we call table. Each entry in table is
a dotted pair (arg . val), in which val is the value of (proc arg). Thus if
344 Mutation
the procedure is the Fibonacci procedure fib and we have already computed
fib for arguments 2, 4, and 6, then table is ((2 . 1) (4 . 3) (6 . 8)).
We can also use proper lists as the entries in the table; in that case, table
would look like ((2 1) (4 3) (6 8)). The dotted-pair representation has
the advantage of being more efficient, since in building the table, only one con-
sing operation is necessary to build the dotted pair (cons 2 1), whereas two
consing operations are necessary to build the list (cons 2 (cons 1 '())).
Similarly, to obtain the second item in the dotted pair, we use cdr, whereas
in the list case, we first have to take the cdr and then take the car. We use
the procedure 1st to access the first item and 2nd to access the second item
of a list. If we are using lists of two items, 1st is then car, and 2nd is cadr.
If the list has three items, we also use 3rd instead of caddr, and if the list
has four items, we include 4th for cadddr. These are easier to read than the
multiple car/cdr chains.
To look up whether fib has already been computed for argument 4, the
procedure lookup is used. The procedure lookup has four parameters, an
object obj, a list of pairs table, and two procedures: success-proc, which
takes one argument, and f ailure-proc, which takes no arguments. If there is
a pair in table that has obj as its first element, then we say that the lookup
succeeded, and we invoke success-proc on that pair. If there no pair
is
in table that has obj as its first element, our lookup failed, and we invoke
f ailure-proc. These two procedures, success-proc and f ailure-proc, are
known as the success continuation and the failure continuation, respectively,
because they tell how to continue the computation when we either find obj
and
In these examples, table is a list of items, each item being a list of three
numbers. The success continuation tells us to return the second element in
the pair. The failure continuation tells us to return 0. In the first example,
4 is found to be the first element in the third pair, so 16 is returned. In the
returned.
(define lookup
(lambda (obj table success-proc failure-proc)
(letrec ((lookup (lambda (table)
(if (null? table)
(failure-proc)
(let ((pr (car table)))
(if (equal? (car pr) obj)
(success-proc pr)
(lookup (cdr table))))))))
(lookup table))))
The code for assoc is easily written using lookup. We have only to take the
identity procedure as the success continuation and the constant procedure
that always returns false as the failure continuation. See Program 11.3.
We are now ready to write the procedure memoize, which takes a procedure
proc as its parameter and returns another procedure that is the memoized
346 Mutation
Program 11.3 assoc
(define assoc
(lambda (obj table)
(lookup obj table (lambda (pr) pr) (lambda () #f))))
(define memoize
(leimbda (proc)
(let ((table '()))
(leunbda (arg)
(lookup arg table
(leunbda (pr) (cdr pr))
(lambda ()
(define fib
(lambda (n)
(if « n 2)
n
(+ (fib (- n D)
(fib (- n 2))))))
We now show two different ways of approaching the memoization of fib. The
first way, which does not lead to the most efficient way of computing it, is to
Evaluate (fib-m 6) first. Since (fib 6) has not yet been computed, fib is
computed recursively to get its value 8. This value is added to the table in the
^ The line (lambda (pr) f^cdr pr)) in the definition of memoize can be replaced by cdr.
Can you explain why?
(define memo-fib
(sHoize (lambda :n)
(if « n 2)
n
(+ (memo--fib (- n D)
(memo--fib (- n 2)))))))
set! line of code for memoize, and table is now bound to ((6 . 8)). When
(f ib-m 6) is evaluated again, the new binding is in effect. In this way, set
changes the value of the lexical binding of the variable table.
If (fib-m 6) is called again, the lookup with argument 6 produces the
pair (6 . 8) and by taking its cdr, we get the answer 8. If this is now re-
peated with (fib-m 100), the first time it is called, the tree recursion will
involve approximately 3(1.7^°°) operations, which is more than 10^^. If, how-
ever, (fib-m 100) is called a second time, the answer is found with just one
lookup in the table, a significant improvement in efficiency since the table
contains only two items at this point. If the table does get large after many
procedure calls with different arguments, the iteration lookup makes the
in
search in the table take time, so there is a trade-off that must be weighed be-
tween the efficiency of the procedure evaluation and the table lookup. Later,
we shall consider the use of vectors to represent the table of stored values.
Random access into the table for our lookups makes the memoizing process
more efficient than iteration through the table from its beginning.
A more practical use for the memoizing procedure can be made if we use
it in the recursive invocations of fib that are made in the definition of the
procedure fib itself. In computing (fib 6) recursively, one computes fib for
arguments 5, 4, 3, 2, and 1. Suppose that the first time any one of these is
found, its (.arg . va/)-pair is added to table. Then whenever fib is called
with this same argument in the tree recursion, the value is already known
from the table lookup and need not be recomputed. This will save time ui
the original computation of (fib 6), and if we call (fib 7), it is the sum of
(fib 6) and (fib 5), both of which are already in the table. This is a great
saving in time. Program 11.5 shows how we write this memoized version of
fib.
It is possible to get a feeling for how much memoizing improves the efficiency
of a computation by using the "timer" defined Program 10.19. Below is in
348 Mutation
above:
Exercise
Exercise 11.1
To see how dramatically memoizing reduces the number of recursive invoca-
tions of fib, trace the procedures fib and memo-fib by placing (display n)
(display " ") before the if expressions. Then invoke the traced procedures
with arguments 6, 7, and 10. Be sure that you understand the results.
Because the elements of a vector can be accessed randomly with equal ease,
vectors can be used effectively for making tables. When a list is used as the
data structure for a table and the table lookup is done with assoc or with
lookup, the iterative search tests each entry until the correct one is found. If
we are looking for an entry that is not near the beginning of the list, the search
is costly in time. In a vector, we can access any entry with equal facility. Let
us rewrite memoize for a procedure that takes a nonnegative integer as its
argument. This time table is a vector in which we store values. We must fix
a largest value for the arguments that we store in the table, since we must
determine whether that value has already been entered into the table. We
use vector-ref for the lookup and item-stored be (vector-rel table
let
the original table.To avoid having to put restrictions on the values assumed
by the procedure, we enter (list val) instead of val into the table whenever
a new procedure value is added. Then item-stored finds a list containing
the value, and to get the value itself, we must take (car item-stored). The
definition of vector-memoize is given in Program 11.6.
(define vector-meBoize
(lajBbda (max-arg)
(lambda (proc)
(let ((table (Make-vector (addl max-arg) '())))
(lambda (eurg)
This memoizing procedure can be used in exactly the same way as the one
we defined earlier for numerical procedures like fib. It is more efficient when
many entries have been made into the table since the random access of the
vector makes lookup faster.
^ This is a disadviuitage of using vectors instead of lists as the data structure for the table.
We shall look at a way of removing this restriction when we discuss hashing in Chapter 12.
350 Mutation
calling the procedure memo-fib, now defined by:
(define memo-fib
( (vector-memoize 100)
(lambda (n)
(if « n 2)
n
(+ (memo-fib (- n 1)) (memo-fib (- n 2)))))))
First, Scheme evaluates the expression val. Then Scheme looks for the lexical
binding of var, and the value of that binding is changed so that var is bound
to the value of val. If var is not bound to some value, then in many implemen-
tations, the set! expression returns an error message, for set ! changes only
existing bindings; it does not create new bindings. It is important to realize
that set ! does this rebinding as a side effect, Scheme set returns an
and in !
(define member?
(lambda (item Is)
(let ((goto (lambda (label)
(label))))
(letrec
( (start
(lambda ()
(cond
((null? Is) «f)
((equal? (car Is) item) #t)
(else (goto reduce)))))
(reduce
(lambda ()
Is. The procedure goto invokes the thunk, which is its argument. The letrec
expression defines the thunks, which control the order in which the statements
are executed in the program. In the last line of the program, we see that the
first thunk invoked is steirt. The body of this thunk is a cond expression
that has three clauses. In the first of these, the condition tests whether the
list Is is empty. If so, #f is returned. The second condition in the thunk
start tests whether the first value in the list Is is item, in which case the
value #t is returned. The else clause in the thunk start moves control to the
body of reduce, where the list Is is assigned the value (cdr Is) and control
is moved back to start. It is important to realize that no variable is passed
to start when control is moved to start. Instead, the value assigned to the
352 Mutation
.
(define while-proc
(lambda (pred-th body-th)
(letrec ((loop (lambda ()
(if (pred-th)
(begin
(body-th)
(loop))))))
(loop))))
variable Is is changed by the set ! statement, and that is the value of Is used
on the next invocation of start. In functional programming, new values are
passed as arguments to procedures, whereas in this imperative programming
style, the new values of variables are given by assignment statements using
mutation procedures such as set !
value it returns is ignored and the value of ans is returned. This definition of
member? follows:
(define member?
(lambda (item Is)
(let ((ans #f))
(while-proc
(lambda (not (or (null? Is) ans)))
(lambda
(if (equal? (car Is) item)
(set ! ans #t)
(set! Is (cdr Is)))))
ans)))
(define swapper
(lambda (a b Is)
(letrec ((loop (lambda (Is*)
(cond
((null? Is*) '())
((equal? (car Is*) a)
(cons b (loop (cdr Is*))))
((equal? (car Is*) b)
(cons a (loop (cdr Is*))))
(else
(cons (car Is*) (loop (cdr Is*))))))))
(loop Is))))
Most languages have a looping mechanism that repeats some operation until
a terminating condition is satisfied. We once again use the procedure nhile-
proc for that purpose. Notice that in the recursive program for swapper,
until Is* is empty, something is consed onto the recursive invocation of loop
354 Mutation
)
with argument (cdr Is*). Thus, that something is added to a return table
on each loop. We accomplish this in our imperative-style program by pushing
that something onto a stack, and when Is* is finally empty, we repeatedly
cons the top of the stack onto our answer and pop the stack until the stack
is empty. Thus our code will have two loops; the first pushes the appropriate
thing onto the stack until the list is empty, and the second pops the stack until
it is empty. We assume here that the stack stk has been globally defined and
is empty when the procedure swapper is called.
(define swapper
(leunbda (a b Is)
(let ((Is* Is) (ans '()))
(while-proc
(lambda () (not (null? Is*)))
(lambda ()
(cond
((equal? (car Is*) a) (push! b))
((equal? (car Is*) b) (push! a))
(else (push! (car Is*))))
; (print-stack)
(set! Is* (cdr Is*))))
(while-proc
(lambda () (not (empty?)))
(lambda
(set! ans (cons (top) ans))
; (writeln "Answer = " ans)
(pop !
; (print -stack)
))
ans)))
Figure 11.10. The stack grows during the first loop, and the answer is being
built before each pop of the stack during the second loop.
In this example, the stack was defined globally and given the name stk.
Each of the stack operations referred to this stack in its definition. If we had
needed two stacks, say stkl and stk2, we would have had to define two sets
of stack operations and use the right ones with each of the two stacks. This
is an inconvenient way of working with stacks, so we shall look further into
this matter in Chapter 12 and develop a better way of implementing stacks
using what is known as object-oriented programming.
The stack is an object that changes with each push! and pop!. An object
that changes with time is said to be in a given state between changes. We
say that such an object has state. Its state can be described by certain
descriptors, called its state variables. In the case of the stack, we can consider
its list representation as a state variable that completely describes the stack.
The mutators such as set and vector-set and the procedures derived from
! !
them such as push! and pop! are used to change the state of objects. In most
of our Scheme programs, we can use procedure applications and recursion and
avoid the use of mutators altogether. There will be times, however, when we
find it convenient to use mutators.
356 Mutation
In mathematics and logic, a function is a rule that assigns to each of its
arguments a certain value, and the presentation in Section 8.5 developed this
point of view. We have been using the term procedure to describe the programs
we have been writing in Scheme. It would have been appropriate to use the
term function as long as we do not have side effects and the value returned by
the function is completely determined by its arguments; that is, the function
returns the same value every time it is invoked with the same arguments. This
is no longer the case when side effects are present. It is more accurate to use
the term procedure instead oi function since side effects are allowed, and this
is the custom in Scheme.
Exercises
above it and on each side of it. The first six lines of the triangle are shown
below:
1
1 1
1 2 1
13 3 1
14 6 4 1
1 5 10 10 5 1
Using a zero-based counting system and denoting the number in the nth
row and the column by (pascal-triemgle n k), (pascal-tricingle 4
kth.
ferent ways k objects can be selected from a list of n distinct objects. This
number is often denoted by [^). The notation n! is used for the factorial of
n, which we compute with the procedure fact. It can be shown that the
number (^) can be computed using the formula f.,(^_f^\,
- Write the definition
of a procedure combinations that uses this formula instead of the algorithm
given in Exercise 11.2. Compare it with pascal-triangle by timing it for the
values of the arguments given in Exercise 11.2. Also compute (combinations
100 50).
Exercise 11.7
Memoize the procedure pascal-triangle in Exercise 11.2 to define a pro-
358 Mutation
)
(define nystery
(lambda (a b Is)
(let ((Is* Is) (ans '()) (goto (lambda (label) (label))))
(letrec
( (push
(Isuabda
(cond
((null? Is*) (goto pop))
((eq? (czu: Is*) a) (push! b) (goto reduce))
((eq? (car Is*) b) (push! a) (goto reduce))
(else (push! (car Is*)) (goto reduce)))))
(reduce
(Isunbda
(set! Is* (cdr Is*))
(goto push)))
(pop
(lambda ()
(cond
((empty?) eins)
(else
(set! ans (cons (top) ans))
(pop !
(goto pop))))))
(goto push)))))
cdr, which is 4. When (cons 3 4) is entered into Scheme, the improper list
(3 , 4) is returned. We call the pointer from the left side of a cons cell the
car pointer and the pointer from the right side of a cons cell the cdr pointer.
The value of (cons 3 (cons 4 5)) is represented by two cons cells, one
for each cons. Figure 11.12(b) shows the box-and-pointer configuration for
360 Mutation
I
I
-j
— >[A' —
1 I
>^ 5
yt
3L >r yr
3 3 l4 5
(a) (b)
cons in an expression creates a new cons cell in which the car pointer points
to the car part and the cdr pointer points to the cdr part of the cell.
»
3
we can indicate this binding by a pointer from the name a to the cons cell
we can think of this as disconnecting the pointer from a to the linked cells
representing (cons 3 '()) and connecting it to the linked cells representing
(cons 4 (cons 5 '())), as illustrated in Figure 11.15(b).
S62 Mutation
>
a —
(a)
4 5
(b)
The cons in the definition of c creates a cons cell (to which c points) in which
the car pointer points to the same cell as does a and the cdr pointer points
to the same cell as does the cdr pointer of the cons cell to which b points.
This is illustrated in Figure 11.16(c). It is clear from this representation that
a and b have not been changed when we defined c, and a => (1 2), b =*
((3) 4 5), and c ==» ((1 2) 4 5).
The procedures set-car! set-cdr , ! , and append! which we discuss next,
,
actually do change the objects to which they are applied. For example, when
we same definitions of a and b given above and
use the illustrated in Fig-
(set-car! b a)
/"
(b)
(c)
then the car pointer of the cons cell to which b points is disconnected and
is made to point to the same linked cell as does a. This is illustrated in
Figure 11.17. Now:
b =» (Cl 2) 4 5)
a =* (1 2)
begin expressions since this mutation is a side effect. Let us compare b and c.
They are equal?, but not eq?. Also the application of set-car had the effect I
of disconnecting the previous car of b (see the dotted box in Figure 11.17)
which is now "garbage" to be recycled in the next "garbage collection." In
general, cells are garbage if they are not pointed to by nongarbage.
The invocation (set-cdr ! pair value) does the same kind of reconnecting
of the cdr pointer of pair so that it points to ua/ite. The box-and-pointer
diagrams in Figure 11.18 illustrate c and d defined by
364 Mutation
a >
/
^
5 |r
5K
(eq? (car w) c) #t
If we next call
(set-cdr! c d)
the cdr pointer of c is changed to refer to d (Figure 11.19) and now c =* (14
5 6). But the side effects of (set-cdr! c d) extend to all objects that have
pointers to c; now w =^ ((1456) 456). Thus care must be taken when
using procedures like set-cair ! and set-cdr! that cause mutations in the list
w 1
w w
{/
1 2 3
> —-^ H /
V >r
4 5 6
c ->
7^
— ji
^ /!i
1 i
I
2 J j
1
d
r
— — ,/ 1
[4 5 6
w >
structure of data objects, but the values that they return are unspecified and
may differ in different implementations of Scheme. Thus to write programs
that are portable (run in various implementations of Scheme), it is necessary
to avoid using the values returned by these procedures.
S66 Mutation
u >
(define append
(lambda (Isl ls2)
(if (null? Isl)
ls2
(cons (cau: Isl) (append (cdr Isl) ls2)))))
and
then append makes a copy of c and changes the cdr pointer of the last cons
cell in this copy to point to d. (See Figure 11.20.) Then:
(define append!
(lambda (Isl ls2)
(if (pair? Isl)
(begin
(set-cdr! (last-pair Isl) ls2)
Isl)
ls2)))
X (1 2 3 4 5 6)
c > (1 2 3)
d (4 5 6)
u ((3 L 2 3) (4 5 6))
We then define append! in Program 11.22. Here last-pair cdr's down the
list Isl until it reaches the last pair in Isl. Then set-cdr! redirects the cdr
pointer to ls2 instead of the empty list. The last line in the begin expression
returns this mutated list Isl. For example, if we apply append! to the two
lists c and d defined above by writing
368 Mutation
>
\N
c — .^ — > >
y-
1 2 3
i
A
U w ^ >
~y
/"
y
/ X —
4 5
w
6
1 2 3
11
U k
W
- W
/
Figure 11.23 'define y (append! C i))
< ( in Figure 11. 2()
then y is obtained by connecting the cdr pointer of the Icist cons cell in c to
d. (See Figure 11.23.) We now have
y ^ (1 2 3 4 5 6)
c => (1 2 3 4 5 6)
d =^ (4 5 6)
X ^ (1 2 3 4 5 6)
u =* ((1 :> 3 4 5 6) (4 5 6))
The last result is a side effect of using append! on c, for the c, which also
appears in the definition of u, has been mutated. The value of x is not changed
because it originally makes a copy of c and has no pointer to c.Thus each
time we have a choice of using one of the procedures set-cao: !
, set-cdr or !
,
append ! , we must decide whether we want a copy of the original lists made
by using suitable procedures of cons, Ceir, cdr, and append or mutations of
the original lists taking into account the possible side effects. We must be
-^ j_
—1
u j]
careful that undesirable side effects do not occur when using set-Cca- ! , set-
cdr!, and append!. In the next chapter, we shall see examples where these
procedures can safely be used because the variables affected are local and the
side effects can be controlled.
Exercises
Exercise 11.11
Draw a box-and-pointer diagram for the following:
Exercise 11.12
Write a let expression in the style of Exercise 11.11 that generates the entire
structure shown in the box-and-pointer diagram in Figure 11.24.
Exercise 11.13
370 Mutation
4
c. Define a procedure that takes a list as its argument and removes all occur-
rences of such cons cells.
Exercise 11.1
Conduct the following experiment, explaining the results:
Exercise 11.15
Let us consider only flat lists in this exercise. We know that we can write a
procedure that determines the length of a list. We allow, however, that the
cdr of the last cell of the list might point back to some portion of the list. For
example,
We can print such lists by invoking (writeln x); however, the printing of the
list will not terminate. Redefine writeln so that if it discovers one of these
lists, it prints something appropriate. For example,
Hint: Define a predicate cycle? which determines if a list is a flat cycle. Also,
reconstruct a list like (abcdecde ...) that has the string " ..." as
the last of its nine elements.
(define test-efface
(lambda
(let ((x (cons 1 '())))
(let ((y (cons 2 x)))
(let ((z (cons 3 y)))
(let ((a (cons 4 z)) (a* (cons 40 z)))
(let ((b (cons 5 a)) (b* (cons 50 a)))
(let ((c (cons 6 b)) (c* (cons 60 b)))
(writeln x y z a a* b b* c c*)
(efface 3 c)
(writeln x y z a a* b b* c c*)))))))))
Exercise 11.17
Using the definition of efface from Exercise 11.16, describe the behavior of
(test-efface2) and (test-elf ace3). Explain the difference.
(define test-efface2
(lambda
(let ((Is (list 5 4 3 2 1)))
(writeln (efface 3 Is))
Is)))
(define test-efface3
(lambda
(let ((Is (list 5 4 3 2 1)))
(writeln (efface 5 Is))
Is)))
372 Mutation
(define smudge
(Icunbda (x Is)
(letrec
((smudge/x
(lambda (Is*)
(cond
((null? (cdr Is*)) Is*)
((equal? (car Is*) x) (shift-down Is* (cdr Is*)))
(else (smudge/x (cdr Is*)))))))
(if (null? Is)
Is
(begin
(smudge/x Is)
Is)))))
(define shift-down
(lambda (boxl box2)
(set-car! boxl (car box2))
(set-cdr! boxl (cdr box2))))
(define test-smudge
(lambda ()
(begin
(set! seen-pairs* (cons pr *8een-pairs*))
(addl (+ (coiont -pairs (c«ir pr))
(count -pairs (cdr pr))))))))
(define test-count-pairs
(lambda ()
gram 10.5.
c. Rewrite count -pairs using local state, which gets changed with set !
Here is a skeleton:
374 Mutation
d. Rewrite count-pairs using private local state. Hint: Look at the skele-
ton below. We must set seen-pairs back to the empty list just prior to
invoking count. Here is a skeleton:
The next seven problems are related. Work them in order and you will learn
The and fourth procedures are left and right. These take a tape
third
and return an equivalent tape except that the character being read is the one
the tape is unbounded in both directions, there is no concern about falling off
(define reconfigure
(lambda (tape character direction)
(if (eq? direction 'left)
(left (overwrite chciracter tape))
(right (overwrite character tape)))))
(define at
(lanbda (tape)
(let ((right-part (2nd tape)))
(.car right-paurt))))
(define overwrite
(lambda (chau: tape)
(let (deft-part (Ist tape)) (right-paurt (2nd tape)))
(let ((new-right-part (cons cheur (cdr right-part))))
(list left-part new-right-part)))))
We have only to define the procedures left and right. Let us consider
what is involved in moving to the right. In our example this would mean that
we are looking at y. If that is so, then the right part would become (y 0).
What would happen to the x? Since it is now to the left of where we are
376 Mutation
)
reading, it would be moved into the left part and would become the first item
in the left part. Here is a first try at right:
(define right
(lambda (tape)
(let (deft-part (1st tape)) (right -part (2nd tape)))
(let ((new-left-part (cons (car right -part) left-part))
(new-right-peirt (cdr right-part)))
(list new-lef t-petrt new-right-part)) ) ))
This is very close to correct, but it might violate the restriction that the left
part and the right part must be non-null. Consider invoking right on the
tape ((y x a b c 0) (0)). Using this incorrect version of right, the new
tape would become ((OyxabcO) ()), and that violates the non-null
condition that each part must satisfy. If new-right-part is the empty list,
we must replace it by (0), which represents the right end of the tape. Here
is the improved version of right:
(define right
(lambda (tape)
(let (deft-part (1st tape)) (right -part (2nd tape)))
(let ((new-left-part (cons (ceir right -part) left-peirt))
(new-right-part (cdr right-part)))
(list new-lef t-peurt (check-null new-right-pairt))))))
(define check-null
(Icimbda (part)
(if (null? part)
(list 0)
part)))
Write the procedure left, and test reconfigure with the procedure below:
(define test-reconfigure
(Isunbda
(let ((tapel (list (list 'a 'b 'c 0) (list 'x 'y 0))))
(let ((tape2 (reconfigure tapel 'u 'right))
(tapeS (reconfigure tapel 'd 'left)))
(let ((tape4 (reconfigure tape2 'v 'right))
(tapes (reconfigure tape3 'e 'left)))
(let ((tape6 (reconfigure tape4 'w 'right))
(tape? (reconfigure tapeB 'f 'left)))
(let ((tapes (reconfigure tape6 'x 'right))
(tape9 (reconfigure tape? 'g 'left)))
(list tapes tape9) ) ))) ))
not ((a b c 0) (x y 0)). Not only must the left part be reversed, but no
O's should appear in the resultant list. Rewrite test-reconfigure so that it
Exercise 11.22
In the procedure test-reconf iguxe from the previous exercise, we used
tapel twice. Generally that does not happen. More frequently, a tape is
When the tape is used in this fashion, we no longer need to make a new copy
each time we reconfigure the tape. For example, we can redefine overwrite
to change the value that at returns just by using set-car!:
(define overwrite
(lambda (char tape)
(let ((right-part (2nd tape)))
(set-csir! right -part char)
tape)))
378 Mutation
)
longer produces the same result. Why? Redefine right and left to use as
few invocations of cons as possible. Test shifter as in [2].
Exercise 11.23
We can write interesting procedures that begin with an empty tape (i.e,
(list->tape '())). Test the procedures below using an empty tape and
determine which ones do not halt:
(define busy-beaver
(letrec
( (loopright
(lambda (tape)
(let ((c (at tape)))
(cond
((equal? c 'a)
(loopright (reconfigure tape 'a 'right)))
(else (maybe-done (reconfigure tape 'a 'right)))))))
(maybe-done
(lambda (tape)
(let ((c (at tape)))
(cond
((equal? c 'a) (reconfigure tape 'a 'right))
(else (continue (reconfigure tape 'a 'left)))))))
(continue
(lambda (tape)
(let ((c (at tape)))
(cond
((equal? c 'a)
(maybe-done (reconfigure tape 'a 'left)))
(else (loopright (reconfigure tape 'a 'right))))))))
loopright)
(define endless-growth
(letrec
((loop
(leunbda (tape)
(let ((c (at tape)))
(cond
((equal? c 0)
(loop (reconfigure tape 'a 'right))))))))
loop))
(define pendulum
(letrec
( (loopright
(Icifflbda (tape)
Exercise 11.24
Each of the procedures in the previous exercise looks about the same. Each
takes a tape as an argument. Then it reconfigures the tape according to
the current character and either returns the reconfigured tape or passes it
along to another procedure. We can think of each cond line as a list of five
elements. For example, the first cond line of continue in busy-beaver could
be represented by: (continue a a left maybe-done). We can interpret
this line as follows: From the state of continue, if there is an a, overwrite it
380 Mutation
)
with an a,move left, and consider only the lines that start with raaybe-done.
The entire busy-beaver procedure could be represented by a list of all the
transcribed cond lines:
(define busy-beaver-lines
'((loopright a a right loopright)
(loopright a right maybe-done)
(maybe-done a a right halt)
(maybe-done a left continue)
(continue a a left maybe-done)
(continue a right loopright)))
We use the convention that we start the computation at (car (car busy-
beaver-lines)). We also assume that halt is self-explanatory.
Using the representation of tapes that we have developed thus far, define a
procedure run-lines that takes a set of lines, like the busy-beaver-lines,
and a tape and returns the same result as (busy-beaver tape). The proce-
dure below will get you started. You need only define the procedures called
by run-lines.
(define run-lines
(lambda (lines tape)
(letrec
((driver
(lambda (state tape)
(if (eq? state 'halt)
tape
(let ((matching-line
(find-line state (at tape) lines)))
(driver
(next-state matching-line)
(reconfigure
tape
(next-char matching-line)
(next-direction matching-line) ))))))
(driver (current-state (car lines)) tape))))
Such a set of lines is called a Turing machine, named for Alan M. Turing.
Turing claimed that with a small set of characters, including the 0, he could
use his machines to compute whatever a computer could. Then he showed
that no one can write a procedure test-lines, like run-lines, that takes
Exercise 11.25
Often it is possible to remove a test by careful design. In the definition of re-
configure, there is a superfluous test. Rewrite busy-beaver replacing 'left
by left and 'right by right, so that a revised definition of reconfigure
works.
(define overwrite
(lambda (char tape)
(let ((left (2nd tape)) (right (3rd tape)))
(list char left right))))
(define right
(lambda (tape)
(let ((char (1st tape))
(left (2ud tape))
(right (3rd tape)))
(list (car right)
(cons char left)
(check-null (cdr right))))))
Use this representation of tapes and test busy-beaver. Then redefine all
necessary procedures so that the use of cons is minimal.
382 Mutation
12 Object-Oriented Programming
12.1 Overview
the message passed to the object, the object does the computation, and the
output is the value returned by the object. In this chapter, we see how such
objects are defined, and we illustrate the use of objects to define such data
structures as stacks and queues.
In Chapters 3 and 5, the concept of data abstraction was discussed and il-
lustrated. We saw there that we can write programs that are independent
of the representation of the data and are based on certain predefined basic
procedures, including the constructors and selectors used on the data type.
The actual representation of the data was then used only in defining these
basic procedures. We develop the idea of data abstraction further by defining
certain objects that are combined with certain operations. It is not necessary
for users to know how these objects and operators are implemented in order
to use them. They only have to know the interface. An example of such an
object is a stack that has associated with it such operations as push! and
pop ! . This offers a degree of security in the handling of data and makes it
possible to change the internal representation of the object without the user's
being aware of any changes. Before looking at stacks and queues, we introduce
the Ccise expression, which makes it easier for us to define the various objects
we shall study.
Scheme provides a special form with keyword case that selects one of a se-
quence of clauses to evaluate based upon the value of an argument (or mes-
sage) that it is passed. To see how case is used, let us first look at a procedure
that tells us whether a letter is a vowel or a consonant. We can define it as
(define vowel-or-consoneint
(lambda (letter)
(cond
((or (eq? letter 'a)
(eq? letter 'e)
(eq? letter 'i)
(eq? letter 'o)
(eq? letter 'u))
'vowel)
(else 'consonant))))
This procedure can also be defined using the special form case as follows:
(define vowel-or-consonant
(leUDbda (letter)
(case letter
((a e i u) 'vowel)
(else 'consonant))))
The value of letter is matched with each of the items (keys) in the list in
the first clause of the case expression. If there is a match, the expression
following the list of keys is evaluated and returned as the value of the case
expression. Thus if letter evaluates to one of a, e, i, o, or u, vowel is
returned. Otherwise, the next clause is evaluated, and since in this case it is
the else clause, consonant is returned. In case letter evaluates to one of the
five vowels, it is more convenient to use the case expression, which matches it
with the possible key values rather than the cond expression, which must list
In the various objects we shall define in this chapter, we shall use internal
representations of the data, which are not supposed to be apparent to the user.
In order to secure the data structures used, we introduce, in Program 12.1,
the procedure f or-eff ect-only, which evaluates its operand to perform the
side effects and then returns the string "unspecified value". Following our
usual convention, we will not display "unspecified value".
12.2.2 Boxes
A box is a place in which a value can be stored until it is needed later. A new
box containing a given initial value is created by the procedure box-maker.
There are five operations that we shall perform on a box. We use one of
these operations to put a value into the box and another to show the value
in the box. The operation that puts a value into the box is called update!
and the operation that shows the value in the box is called show. Another
useful operation, called swap! ,
puts a new value into the box and returns the
old contents of the box. The operation called reset resets the value stored
!
in the box to its initial value. The ability to perform a reset operation is
somewhat unusual. The operation type, specified for all objects, tells what
kind of object is being sent a message. In this case the type is "box". In
general, the operations that are performed on an object are called methods.
In the case of a box, there are five methods: update !
, show, swap !
, reset !
and type.
The objects, such as boxes, are themselves procedures. To apply one of
themethods to an object, we invoke the object on the (quoted) name of the
method followed by any additional arguments appropriate for that method.
We then say that we send the name of the method and any additional argu-
ments as a message to the object. We can use the call structure:
where object is sent the message consisting of the quoted method name and
zero ormore operands. On the other hand, we find it more suggestive and,
in fact,more flexible to introduce the procedure send, which is used to send
the message to the object. When we use send, we use the call structure
the actual mechanism for constructing boxes after looking at the example.
the exclamation mark on the word update, and an error was signaled. The
sending of these quoted method names and arguments as messages to the
objects leads to a style of programming referred to as message-passing style.
In Program 12.2, we define box-maker. It takes as its argument an initial
value stored in the box. It returns a procedure that takes an arbitrary num-
ber of arguments and is hence defined using the unrestricted launbda whose
parameter list is denoted by msg. Each invocation of box-maker returns an
object that we refer to as a box. Thus, in our experiment presented above,
box-a and box-b are examples (or instances) of boxes. Also, in [3] , the mes-
sage consists of the single item ' show, whereas in [5] , the message consists
of two items, the method name 'update! and the operand 3. In the code
given below for box-maker, we use 1st and 2nd to denote car and cadr.
(define box-maker
(leunbda (init- value)
(let ((contents init-value))
(lambda msg
(case (1st msg)
((type) "box")
((show) contents)
((update!) (for-ef feet-only (set! contents (2nd msg))))
((swap!) (let ((ans contents))
(set! contents (2nd msg))
ans) )
(define delegate
(lambda (obj msg)
(apply obj msg)))
^ When an object cannot respond to a message, there are mechanisms other than delegation
which have been developed. One common mechanism is inheritance. We have chosen to
use delegation instead of inheritance; however, aJl programs expressible with inheritauice
are also expressible with delegation.
(define base-object
(lambda msg
(case (1st msg)
((type) "base-object")
(else invalid-method-name-indicator) ) )
(define send
(lambda args
(let ((object , (car args)) (messag e (cdr args)))
(let ((try (apply object messag e)))
(if (eq? inval id-method-name- indicator try)
(error "Bad method name:" (car nessage) 1
"sent to object of 1
(object 'type)
"type.")
try)))))
The procedure send then generates the appropriate invocation of error. The
definitions of base-object and send are contained in Programs 12.4 and 12.5.
We shall define many diff'erent types of objects in this chapter using object
makers similar to box-maker. These will each contain an else clause that
must handle method names for which there is no match. One of the major
advantages of using send is that all these else clauses will have exactly the
same call structure
and send takes the appropriate action. When writing the definitions of the
object makers and when using the objects, we must remember that:
(define box-maker
(lambda (init-value)
(let ((cell (cons init-value "any value")))
(lambda msg
(case (1st msg)
((type) "box")
((show) (car cell))
((update!) (for-ef feet-only (set-car! cell (2nd msg))))
((swap!) (let ((sms (cju: cell)))
(set-car! cell (2nd msg))
ans))
((reset!) (for-ef feet-only (set-car! cell init-value)))
(else (delegate base-object msg)))))))
12.2.3 Counters
A counter is an object that stores an initial value and each time it is called,
the stored value is changed according to some fixed rule. The counter has two
arguments: the initial value stored and the procedure describing the action
to be taken each time the counter is updated. For example, (counter-maker
10 subl) is a counter with initial value 10 that decrements the counter by
1 when it is updated. The counter responds to the method names: type,
update !
, show, and reset !The definition of counter-maker follows:
.
(define counter-maker
(lambda (init-v«ilue \in«iry-proc)
(let ((total (box-meJcer init-value)))
(lambda msg
(case (1st msg)
((type) "counter")
((update!) (let ((result (imary-proc (send total 'show))))
(send total 'update! result)))
((swap!) (delegate base-object msg))
(else (delegate total msg)))))))
The counter locally defines the box total, which contains the initial value
stored in the counter. When the counter receives the message consisting of the
method name update!, the unary update procedure unsury-proc is applied
to the value stored in the box total to obtain the new value which is then
stored in total. For example, if we wanted the counter to count up by 1 each
time it is updated, we can use addl as the unary update procedure. To create
a counter with initial value that increases the stored value by 6 each time
it is updated, we write:
The counter is not supposed to respond to swap ! . Thus if such messages are
sent to a counter, they are delegated to base-object rather than to a box,
which does respond to swap ! . Since the counter responds to the messages
show and reset ! the same as the box total, the else clause merely passes
these messages to total. Thus the message show displays the value currently
stored in the counter, and reset! resets the counter to its initial value. The
fact that the response of the counter to these messages can be found by passing
them to the box is called delegation. The work of the counter is "delegated"
to the behavior of the box.
The approach of catching the method names that are to be disabled, like
swap!, is way of supporting the interface. Another alternative is to
only one
catch all method names to be enabled. Thus, we can rewrite count er-
the
metker using this view. As long as we delegate to the base object all the
method names that are meaningless, we can use either approach. On one
hand we are throwing the illegal method names out (i.e., disabling them), and
on the other, we are delegating the legal ones (i.e., enabling them). In any
(define counter-maker
(lambda (init -value unary-proc)
(let ((total (box-maker init-value)))
(lambda msg
(case (1st msg)
((type) "counter")
((update!) (send total 'update!
(unary-proc (send total 'show))))
((shoB reset) (delegate total msg))
(else (delegate base-object msg)))))))
event, both have the same effect, and each has aspects that recommend it. If
we are delegating to an object with many legal method names, and only a few
illegal ones, then we should disable illegal method names; otherwise we are
free to choose to enable legal method names. A version of counter-maker,
which enables legal method names, is presented in Program 12.8.
12.2.4 Accumulators
An accumulator is an object that has the initial value init-value. Each time
it receives a message consisting of the method name update! and a value v,
For example, if ace is an accumulator that initially stores the value 100 and
has subtraction (-) as its binary update procedure, it is defined by
and
causes the number 90 to be stored in ace. If we then update ace with 25, we
write
(define accumulator-meiker
(IcUDbda (init-value binary-proc)
(let ((total (box-maker init-value)))
(leimbda msg
(case (1st msg)
((type) "accumulator")
((update!
(send total 'update!
(binary-proc (send total 'show) (2nd msg))))
((swap!) (delegate base-object msg))
(else (delegate total msg)))))))
The accumulator uses a box, called total, to store its values. In addition to
responding to the message consisting of update ! and a value, it uses delegation
to pass such messages as show and reset ! to the box total. Program 12.9
contains the code for accumulator-maJcer.
12.2.5 Gauges
and
(send g 'up!)
(define gauge-mciker
(lambda (init- value unaury-proc-up uneory-proc-down)
(let ((total (box-maker init-value)))
(lambda msg
(case (1st msg)
((type) "gauge")
((up!) (send total 'update!
(unsury-proc-up (send total 'show))))
((down!) (send total 'update!
(unary-proc-down (send total 'show))))
((swap! update!) (delegate base-object msg))
(else (delegate total msg)))))))
(send g 'down!)
returns the number stored in g to 10. Program 12.10 contains the definition
of gauge-maker.
Exercises
updated, it compares the value stored with a new value and stores the majc-
imum of the two. Then test acc-meix by updating it in succession with the
numbers 3, 7, 2, 4, 10, 1, 5 and find the maocimum by passing acc-max the
shov message.
the predicate, then a reset occurs. For example, if the predicate is (lambda
(n) (and (> n 0) (< n 100))) and we try to bring the restricted counter
up to 105, it will reset to its initial value.
Exercise 12.5
Define the hour hand of a 12-hour clock as a restricted counter. (See the
preceding exercise.)
Exercise 12.6
Define a 12-hour clock that has both a minute and an hour hand. This clock
is to be constructed from two objects. One of them will be the 12-hour
clock, which displays only its hour hand, and the other, the minute hand,
will be built using a modified restricted counter. Such a counter is created
using modif ied-restricted-counter-meOcer, which includes an additional
argument. This new argument is a reset procedure that is invoked in place of
the built-in reset in the restricted-counter-maker. When the minute hand
of the clock is about to pass to 60 minutes, the reset procedure is used not
only to reset the minute hand to but also to update the hour hand. Do not
forget to initialize the clock. The new clock is itself to be an object created by
the procedure of one argument, clock-maQter, that responds to two messages:
show and update!. (See the preceding exercise.)
Exercise 12.7
As was done in Chapters 8 and 9, tag the objects by adding object-tag as
"object". Then define the simple procedures object? and make-object.
Wrap meike-object around (lambda msg ) and redefine send. . . .
Exercise 12.8
Is it possible to implement an accumulator with a counter-maker instead of
a box-maker? Is it possible to implement a counter with an accumulator-
mcQcer instead of a box-maker?
• push !
, which adds an item to the top of the stack.
• top, which returns the item at the top of the stack.
• pop !
, which removes an item from the top of stack.
• size, which returns the number of items on the stack.
An experiment with stacks is given in Figure 12.11. The two stacks, r and
s, are created in [1] and [2]. In the definitions of r and s, we see that
stack-maker is a thunk, that is, a procedure of no arguments. Its definition
Exercise
Exercise 12.9
In arithmetic, parentheses are used to form groupings of numbers and oper-
ators. For example, one writes 3*(4 + 2). In more complicated expressions,
three different kinds of separators are used to form groupings: parentheses '(',
')', brackets '[',']', and braces '{','}'. Here is an expression that uses all three
13 + 5*{[14-3*(12-7)]- 15}
the right symbol just encountered. If they are of different types, the nesting
is not correct. You can model the arithmetic expression as a list of numbers,
(define stack-meJ^er
(lambda
(let ((stk '()))
(lambda msg
(case (Ist msg)
((type) "stack")
((empty?) (null? stk))
((push!) (for-ef feet-only
(set! stk (cons (2nd msg) stk))))
((top) (if (null? stk)
(error "top: The stack is empty.")
(car stk)))
((pop!) (for-ef feet-only
(if (null? stk)
(error "pop!: The stack is empty.")
(set! stk (cdr stk)))))
((size) (length stk))
((print) (display "TOP: ")
(f or-each
(Icunbda (x)
(display x)
(display " "))
stk)
(newline))
(else (delegate base-object msg)))))))
operators, and grouping symbols. Since Scheme uses these symbols as special
characters, one cannot usethem as grouping symbols in the list modeling the
arithmetic expression. Thus use the strings "(",")", "C", "]", "{", and "}"
in place of the grouping symbols. The above arithmetic expression, in this
Test your program on the examples given here and on several additional tests
you devise, some correctly and others incorrectly nested.
called the front. People waiting in line for service normally form a queue in
which new people join the line at the rear and people are served from the
front. Similarly, processes waiting to be run on a computer are put into a
queue to await their turn. Stacks are called LIFO lists because the last one in
is the first one out. Queues are called FIFO lists because the first one in is the
first one out. Adding an item to the rear of the queue is called enqueuing the
item, and removing an item from the front of the queue is called dequeuing.
We implement a queue as an object with the following methods:
• dequeue !
, which removes the item from the front of the queue.
• size, which returns the number of items in the queue.
end of the list, so we can make a list of the element and append that onto
the end of the queue. The code for such an implementation is presented in
Program 12.13.
The implementation using lists as the data structure for the queue produces
the results we want, but it does it inefficiently. The trouble is that when we
enqueue an item, we use append! which must cdr down q until the last pair
,
and then we attach the cdr pointer to the list containing the new item. The
longer the queue, the more "expensive" it is to cdr down q to get to the last
pair. It would be better to have an implementation that could attach the new
item to the end of the queue without having to cdr down the whole queue.
We accomplish this by introducing a second pointer called rear, which points
to the last cons cell in the queue. When the queue is empty, the pointer q
points to a cell formed by (cons '
() '
() ), and rear also points to that cell.
(define queue-m8iker
(lambda ()
q)
(newline))
(else (delegate base-object msg)))))))
Exercises
Exercise 12.10
Add a message to the queue defined in Program 12.15 called enqueue-list
/ w w
(a)
1 2
\^
rear
^
/ p F w
/
(b)
>' >r >'
I 2 3
that takes as an argument a list Is and enqueues each of the elements of the
list to the queue preserving their order. For example, if the queue a contains
the elements 1, 2, 3, with 1 at the front, and if Is is (list 4 5 6), then
after invocation of (send a 'enqueue-list ! Is), the queue a contains the
elements 1, 2, 3, 4, 5, 6 with 1 at the front. Do not use append!. Why?
Exercise 12.11
Revise the definition of queue-maker in Program 12.15 to include a message
enqueue-maoiy ! that enqueues any number of items at one time. For example,
(send a ' enqueue-many ! 'x 'y 'z) has the same effect as
(begin
(send a 'enqueue! 'x)
(send a 'enqueue! 'y)
(send a 'enqueue! 'z))
the front onto the queue. Now dequeue the queue, so that what was at the
front is now at the rear of the queue. Repeat this operation of consing the front
of the queue to the list, enqueuing the front of the queue so that it is at the
rear, and then dequeuing the queue, until the unique element you enqueued
(define queue-maker
(lambda ()
(f or-each
(lajnbda (x)
(display i)
(display " "))
(cdr q))
(newline)
(else (delegate base-object msg))))))))
reaches the front. When it is dequeued, you have a list of the elements that
are in the queue, and the queue is intact.
Exercise 12.13
Rework the previous problem with the method name size enabled.
Exercise 12.14
In the first version of a queue given in this section, the message enqueue!
contains the code (append! q list-of-item). Discuss the correctness and
the efficiency of the code for a queue if that line of code is replaced by (append
q list-of-item) or by (set! q (append q list-ol-item)).
In the previous sections, we defined both the stack and the queue as objects.
we used lists. In the case of the
In the internal representation of these objects,
queue, we used pointers to keep track of the front and the rear of the queue.
There is another way of treating stacks and queues that is more elegant. It
makes use of a data type known as a circular list. In this section, we first
implement circular lists as objects and then use them to define both the stack
and the queue, making use of delegation to take advantage of the properties
of the circular list.
In an ordinary list, the cdr pointer of the last cons cell points to the empty
list. This is denoted by placing a diagonal line in the right hand side of the
last cons cell. If, instead, the cdr pointer of the last cons cell of the list points
back to the first cons cell in the list, we say that the list is a circular list. The
marker
1
1 1
/
r
^
(a)
<
b a
marker
——
/
M _
1
1
^
1
—
r^ ^^ n^ -^ 1
(b)
d c b a
box and pointer diagram for a circular list containing the three items c, b.
and a is shown in Figure 12.17a. Note that marker is a pointer to the cons
cell whose car is a. Then to make the list circular, (cdr marker) points back
to the cell whose car is c. To add an item d to this circular list, we cons d to
(cdr meorker) and then reset the cdr pointer of marker to point to the cons
cell with d as its car. (See Figure 12.17b.) Thus inserting d into a nonempty
circular list can be accomplished by invoking:
Similarly, to remove d from the resulting circular list, we note that (cdr (cdr
marker)) does not contain the item d. so we only have to write
beginning of the list. The item to which the cdr pointer of marker points
is referred to as the head of the circular list. As an object, a circular list
responds to the following messages:
• delete !
, which removes the head of the circular list.
• move!, which shifts the marker to point to the head of the circular list,
meirker is locally defined to be the empty list, and when the method name
empty? is received, it tests whether msorker is the empty The message
list.
sent to insert an item into the circular list consists of two parts, the method
name insert! and the item to be inserted. There are two cases to consider
when inserting an item. If the list is empty, we first make a list consisting of
the item to be inserted and then change marker to point to that list. Then
we have to make the list circular, so we make the cdr pointer of meirker point
back to meirker itself. We now have a circular list containing only the one
item we inserted.
On the other hand, if the list is not empty, we use the fact that (cdr
meirker) points back to the head of the list when we cons the item to be
inserted (that is, (2nd msg)) onto (cdr meirker). Once we have added the
new item to the head of the list, we reset the cdr pointer of maorker to point
to the cell containing the new item, which becomes the new head of the list.
We use the word head in spite of the fact that a circular list does not have a
head or a tail. However, we may think of the cdr pointer of the cons cell to
which mairker points as pointing back to the head of the list to make the list
circular. And we may think of marker itself as pointing to the last cell in the
list.
(define circular-list-maker
(lambda ()
(let ((meurker '())
(size-gauge (gauge-maker addl subl)))
(lambda msg
(case (1st msg)
((type) 'circular list")
((empty?) (null? marker))
((insert !) (send size-gauge 'up!)
(for-ef feet-only
(if (null? marker)
(begin
(set marker (cons (2nd msg)
I
'
()))
(set-cdr! marker marker))
(set-cdr! marker (cons (2nd msg] (cdr marker))))))
( (head) (if (null? marker)
(error "head: The list is empty.")
(ceir (cdr meurker))))
((delete !) (for-ef feet-only
(if (null? marker)
(error "delete!: The eirculeu: list is empty.")
(begin
(send size-gauge 'donnl)
(if (eq? maurker (cdr marker))
(set ! marker '
())
(set-cdr! marker (cdr (cdr marker))))))))
( (move ! (for-effeet-only
(if (null? marker)
(error "move!: The circular list is empty.")
(set! marker (cdr marker)))))
((size) [send size-gauge 'shoff))
((print) (if (not (null? marker))
(let ((next (cdr marker)))
(set-cdr! marker '())
(for-eaeh (leunbda (x) (display x) (display " "))
next)
(set-cdr! marker next)))
(newline)
(else (delegate base-object msg)))))))
(define stack-maker
(laabda ()
In order to avoid this in the case of print, we use the trick of temporarily
resetting the cdr pointer of meirker to point to the empty list. Then the
list is no longer circular, and we can use f or-each without fear of looping
indefinitely.
We are now ready to look at the definitions of stack and queue making use
of a circular list. In implementing the stack, a circular list is used and the
marker stays fixed. When the stack receives a push! message, it sends it to
the circular list as an insert ! message. Similarly, the pop! message is sent to
the circular list as a delete! message. When the print message is received
by the stack, the word TOP: is first printed, and then the message is sent to the
circular list. The stack messages size and empty? are delegated to the circular
list. The code for stack-maker using a circular list is in Program 12.19.
the cell containing the new item. Again, most of the queue operations are
delegated to the circular list. The code for queue-maker making use of a
circular list is given in Program 12.20.
(define queue-maker
(lambda ()
Exercises
Exercise 12.15
Redefine the stack-maker and queue-maLker procedures presented in Pro-
grams 12.19 and 12.20 so that, instead of the illegal method names being
disabled, the legal method names are enabled.
Exercise 12.16
Draw the box-and-pointer diagrams for a stack implemented using a circular
list. Start with the empty stack, push on the items a, b, c, and d, and then
pop these four items. Show the box and pointer diagrams for the successive
stages as the stack increases and decreases in size.
Exercise 12.17
Make the same sequence of box and pointer diagrams as in the previous ex-
Exercise 12.18
Redefine circular-list-maker in Program 12.18 keeping a local variable
that is initialized to zero to keep the size of the circular list without using a
gauge. Then do it without any local variables.
Exercise 12.19
When building a circular list, it is not necessary to build a circular structure.
Instead, the method names, which rely on the circular structure, must be
Exercise 12.20
Add amethod reverse to the circular-list-maJcer that reverses the cir-
cular list in such a way that the cdr pointer of each cons cell is changed to
point to the previous cell in the list instead of the next cell. The diagram
in Figure 12.21 shows a circular list containing four items before and after
reversing. As in the diagram, be sure your method moves the marker.
marker
marker
^ \^
i i i
>r y f y r >r
a b c d
\ \ \
When we update a bucket, if the key is present, then the value associated
with the key is the argument to an updating procedure. The value returned
by this invocation of the updating procedure determines the new value to be
associated with this key. If the key is not present, then the new value to be
associated with this key is determined by invoking an initializing procedure.
The message lookup is like the procedure lookup introduced in the previ-
ous chapter for tables. In that use, we invoke (lookup key table success
fail), and in the object-oriented view, we invoke (send bucket 'lookup
key success fail). Thus if there is a value associated with key, that value
is passed to success, and if key is not in the table, fail is invoked on zero
arguments.
For update! messages there is some similarity with lookup because there
are separate responses to the existence or nonexistence of the key in the ta-
ble. The call structure for update! is (send bucket 'update! key proc-
if -present proc-if-absent). Again a search of the bucket for the key
occurs. If key exists with associated value, vai, that value is replaced with
the result of evaluating (proc-if -present val). If key does not exist, it is
[5] (send b 'update! 'a (lambda (x) (addl x)) (lambda (x) 0))
[6] (send b 'lookup 'a (lambda (x) x) (lambda () 'no))
1
[7] (send b 'update! 'q (lambda (x) (+ 2 x)) (lambda (x) 1000))
[8] (send b 'lookup 'q (lambda (x) x) (lambda 'no))
1000
[9] (send b 'update! 'q (lambda (x) (+ 2 x)) (lambda (x) 1000))
[10] (send b 'lookup 'q (lambda (x) x) (lambda 'no))
1002
[II] (send b 'update! 'q
(leimbda (x)
(send b 'lookup 'a (lambda (y) (- x y)) (lambda () 'no)))
(lambda (y) 'no))
[12] (send b 'lookup 'q (lambda (x) x) (lambda () 'no))
1001
Exercise
Exercise 12.21
The two invocations memoize can be simplified to one by adding
of send in
a new method name to bucket-meiker (see Programs 12.23 and 12.24) that
combines the update and lookup into one operation and thus avoids one of the
two searches. Rewrite bucket-meiker to run the definition of raemoize below.
(define memoize
(lambda (proc)
(let ((bucket (bucket-meiker)))
(leuabda (arg)
(send bucket 'update! -lookup arg (lambda (val) val) proc)))))
Requiring no upper bound on the size of a bucket has its own cost. As
the bucket gets bigger, we discover that the search for updating and looking
information up in the bucket gets more and more expensive. Let us consider
another program that uses a bucket. Suppose we have a list of strings, like
(define bucket-maker
(lanbda ()
(define memoize
(lambda (proc)
(let ((bucket (bucket-maker))
(lambda (arg)
(send bucket 'update! arg (lambda (val) val) proc)
(send bucket 'lookup <urg
the contents of a book. We would like to find the word count frequency of
the articles a, an, and the and possibly some others. We could solve this as
follows:
associated value. To see how many times the three strings "a", "an", and
"the" appear in the text, we write:
(map
(lambda (s)
(cons s (send word-frequency-bucket 'lookup s
(lambda (v) v)
(lambda () 0))))
'("a" "an" "the"))
the remainder with the size of the vector. Here is how to create hash tables.
(define hash-table-maker
(lambda (size hash-fn)
(let ((v ((vector-generator (lambda (i) (bucket--maker))) size)))
(lambda msg
(case (1st msg)
((type) "hash table")
(else
(delegate (vector-ref v (hash-fn (2nd msg))) msg)))))))
An empty bucket is placed at each index of the vector, v. Then, using the
key, an index is determined by applying the hash-fn to the key. The value
at that index is a bucket that responds to the same messages as heish tables.
By same information is
delegating to the bucket the original message, the
forwarded to the bucket. We now write the new definition of memoize using
a hash table in Program 12.26. In order to write this new memoize we only
need to supply arguments to hash-table-maker. Everything else remains
unchanged. This version of memoize is restricted to numerical data since
its associated hash function invokes remainder on its argument. The hash
function can be as general as the problem for which it is being used demands.
(define memoize
(let ((hashf (lambda (x^ (remainder x 1000))))
(let ((h (hash-table-maker 1000 hashf)))
(lambda (proc)
(lambda (arg)
(send h 'update arg (lambda (v) v) proc)
(send h 'lookup arg (lambda (v) v) (lambda #f)))))))
of each string into an integer. The code that assigns to each keyboard char-
acter a unique integer (see Appendix Al) provides us with just the help we
need. Scheme has a procedure string-ref that takes a string and an inte-
ger as arguments and returns the character in the string having that integer
as its index. Scheme also has the procedure char->integer, which takes a
character as its argument and returns the integer associated with that char-
acter. We shall study the character data type more fully in Chapter 15. We
use these two procedures now to define the hash function for the procedure
word-frequency:
(define word-frequency
(let ((naive-hash-function
(lambda (s)
(remainder (char->integer (string-ref s 0)) 26))))
(let ((h (hash-table-maker 26 naive-hash-function)))
(lambda (string-list)
(f or-each
A popular hash function for strings is one that sums the (char->integer
(string-ref s i)) for i = to (subl (string-length s)) and finds the
remainder with the length of the vector. The important aspect of the choice
of hash function is that it must spread the data randomly into the buckets so
that each bucket carries its load.
The advantage of hash tables is that when order is not important, a table
can be stored in a vector so that retrieval and updating are far more effi-
(define neH-bucket-maker
(lambda ()
This hash table is as inefficient as a bucket. We chose the vector too small,
and we chose the hash function too naively. Of course, this would never be
done. In practice, most systems discourage the user from worrying about the
size of the hash table or the nature of the hash function.
Exercises
Exercise 12.22
Construct a list of strings from some paragraph in this section, and run word-
frequency over that list. Determine how many of each of the articles o, an,
and the were used.
Exercise 12.23
Using the list of strings from the previous exercise, introduce a hash function
that uses a large prime number for the vector length and uses the sum of
integers corresponding to characters hash function as described in this section.
Exercise 12.24
Include a message re-initialize ! in the definition of bucket-maker and
hash-table-maker. In both cases, this method returns the object to its
initial state.
Exercise 12.25
Lists that can only grow can get expensive.
a. Include a remove ! message in bucket-maker that removes the key and its
associated value from a bucket. The operation guarantees that if b is a
bucket, then the following expression is always false.
(begin
(send b 'remove! key)
(send b 'lookup key (lambda (v) #t) (lambda () #f)))
is always false.
(begin
(store! b key value)
(send b 'lookup key (leuobda (v) (equal? value v)) (lambda () #f)))
Exercise 12.27
Include an image message in bucket-maker whose value is a list of the key-
value pairs. Design it so that in the event of a subsequent update to an existing
key, that update will not mutate the list previously returned by the image
message. If b is a bucket and (send b 'lookup key number? (lambda ()
is always false.
Exercise 12.28
Using the previous exercise, include an image message in hash-table-maker
whose value is a list of key- value pairs. Design it so that in the event of a
subsequent update to an existing key, that update will not mutate the list
The next four problems are related. Work them in order and you will discover
(define theater-maker
(lEunbda (capacity)
(let ((ticket-line ( queue -maker )
(vacancies (gauge-maker capacity addl subl)))
(lambda msg
(case (1st msg)
((type) "theater")
((enter!) (if (zero? (send vacancies 'show))
(display "doors closed")
(begin
(send ticket-line 'dequeue!)
(send vaceuicies 'down!))))
((leave!) (if (< (send vacancies 'show) capacity)
(send vacancies 'up!)
(error "leave!: The theater is empty.")))
(else (delegate ticket-line msg)))))))
Exercise 12.30
In theater-maker, suppose we would like to know how many seats are vacant
for the next showing. We cannot find this out without introducing a message,
say show, in the definition of theater-maker. See the code below. Why must
we include the extra message?
(define theater-maker
(lambda (capacity)
(let ((ticket-line (queue-maker))
(vacancies (gauge-maker capacity addl subl)))
(lambda msg
(case (1st msg)
((type) "theater")
(define combine
(lambda (f g)
(lambda msg
(let ((f-try (delegate f msg)))
(if (eq? invalid-method--name- indicator f-try)
(delegate g msg)
f-try)))))
The returned procedure will delegate a message, in order, to the two objects
until it finds one that does not return invalid-method-name-indicator. If
f is not such an object, it invokes g. The procedure combine takes only two
arguments. Rewrite combine to take two or more arguments. Below we have
changed theater-maker to use combine so that the vacancies messages will
be delegated too. The result will be multiple delegation, which will delegate
Exercise 12.31
The multiple delegation used with combine in the previous exercise is some-
times dangerous because method names are symbols. What would happen
if show were used instead of front as the message for looking at the first
and
(delegate (combine vacancies ticket-line) msg)
Exercise 12.32
In the interests of security, we would like to disable some operations from
delegation. For example, we would like to keep anyone from resetting the
gauge. This would correspond to yelling "fire," clearing the loge before the
showing, and then allowing just the current contents of the ticket line to enter
the loge. That would not be fair. The patrons who were already in the
loge would have paid without receiving any entertainment. Or perhaps an
update! message might be sent so that everyone might think the loge was
full. Such skullduggery is possible with the current configuration of theater-
The next six problems are related. Work them in order, and you will discover
some interesting generalizations of objects as we have defined them in this
chapter.
Exercise 12.33
Consider a new definition of send.
(define send
(lambda args
(let ((try (apply (car args) args)))
(if (eq? inval id-method-name- indicator try)
(let ( (object (car args)) (message (cdr args )))
(error "Bad method name :
" (car message)
"sent to object of"
(object object ' type)
"type."))
try))))
According to this definition, uses of send are the same as before, but each mes-
sage includes the receiver of the message as the first element of the message.
Here is an example that uses this send to build counter-maker:
The variable message contains the receiver as its car and the original msg as
its cdr. When we delegate, we use the whole message. Rewrite box-maker
so that this definition of coxmter-maker works. Be sure to redefine base-
object.
Exercise 12.34
Below is the definition of cartesicoi-point-maker:
(define cartesian-point-maker
(lambda (x-coord y-coord)
(lambda message
(let ((self (ceur message)) (msg (cdr message)))
(case (1st msg)
((type) "Ccurtesiam point")
((distance) (sqrt (+ (squaire x-coord) (squzure y-coord))))
((closer?) (< (send self 'disteince) (send (2nd msg) 'distance)))
(else (delegate base-object message)))))))
Exercise 12.36
Suppose that we always create points at the origin (0,0). Then we could add
a method name moveto ! that would take an x-coordinate and a y-coordinate
as arguments. In addition, we want a list of the two current coordinates.
Add method names current-coordinates and moveto to the definition
the !
Exercise 12.37
Using the definition of your solution to cartesizin-origiii-meLker from the
previous exercise, fill in the gaps in the experiment below.
Exercise 12.38
Fill in manhattsm-origin-maker below, but do
the rest of the definition of
not use p. Test closer? on a Manhattan point and a Cartesian point that
have both been moved to (6.0,1.0).
(define manhattan-origin-maker
(lambda ()
(let ((p (cartesian-origin-maker)))
(lambda message
(let ((self (car message)) (msg (cdr message)))
(case (1st msg)
((type) "M2mhat tan point")
((distance) ? )
13.1 Overview
One of the many uses of computers is in simulation, that is, in the modeling
of real-world phenomena with the computer to study how varying the con-
ditions (parameters) affects the behavior of the system. We generally select
characteristics of the system to be modeled and define objects and actions
that enable a computer program to mimic the real world. If the real-world
behavior is adequately described in the computer program, the results of the
program should predict what happens in the real-world situation. In many
we are studying is unwieldy and does not lend it-
instances, the actual system
self to experimentation. By using computer simulation, we can see the effects
of parameter changes without tinkering with the actual system. Simulation
is extensively used in decision making in government, business, and industry.
We shall illustrate the use of object-oriented programming in a simulation
of a gasoline station. We have selected this example because we can build a
fairly realistic model for it with relatively few parameters. It also gives us an
opportunity to use a number of the data structures introduced in Chapter 12,
13.2 Randomness
In our gas station simulation, the customers have a choice of full service
or self-service. We estimate the percentage of the customers that choose self-
service, say, 75%, so that 25% select full service. Then when a customer
arrives, we must decide whether he wants self or full service. For lack of
better information, we could toss two coins, and if both come up heads, we
assume that he wants full service. We accomplish this coin-tossing ploy in the
(define unif-rand-veur-O-l
(let ((big 1000000))
(lambda ()
In an actual gas we cannot always tell exactly when the next cus-
station,
tomer will arrive. The variable that tells us when the customers arrive is also
a random variable. It is shown in probability theory that the time between
the successive arrival of customers is a random variable with an exponential
distribution.^ (See Program 13.2.) That is, the time between the arrival of
successive customers can be taken to be a log - where a is the average time
next customer, using the parameter av-arr-time, which is set when the pro-
gram is initialized, and rounding the resulting time to be an integer with 1
(define airrival-time-generator
(Ijunbda (av-arr-time)
(+ 1 (round (exponential -random-variable (- av-arr-time 1))))))
bought on the horizontal axis and the number of people who bought that
many gallons on the vertical axis, we would get the well-known bell-shaped
curve known as the normal distribution. This distribution has the property
that its highest probability is at the mean (or average) value m, which we
shall take as 12 gallons. There is also the standard deviation s, a number
that indicates the fatness of the bell-shaped curve. About two-thirds of the
purchases fall in an interval, which extends a distance s on each side of the
mean m. In our case, we shall take the standard deviation to be 4 gallons, so
that about two-thirds of the customers purchase between 8 and 16 gallons.
m + s yj(u,- — .5)
t=i
(define normal-random-variable
(lambda (meein std-dev)
(letrec ((compute (lambda (i)
(if (zero? i)
(+ (- (unif-rand-var-0-1) .5)
(compute (subl i) ))))))
(+ mean (* std-dev (compute 12))))))
(define gallons-generator
(lambda ()
(maix 1 (round (normal -remdom-vzuri able 12 4)))))
^ We always use
1 2 uniform random v2Lriables to generate a normal random vau-iable because
itcan be shown that the mean of any uniform r2indom v^lriables u on the interval to 1 is .5,
and its standeird deviation is l/\/T2. By adding 12 independent uniform rjuidom variables,
we get an approximation to a normal reindom veiriable with meain .5 and stand2Lrd deviation
1. We subtract .5 in each term to get the meam to be and multiply the resulting sum by
5 to get a steindard deviation of s 2uid add m to get a meem of m.
Exercises
Exercise 13.1
In the game of odds and evens, two coins are tossed. If the result of a toss is
one head and one tail, we call it odds. Otherwise, we call it evens. Write a
program to simulate this game for 1000 tosses and determine the number of
odds and evens that occur.
Exercise 13.2
While riding in a car with a friend, he proposes a wager that involves keeping
track of the last two digits of the license plates of passing cars. He bets you
$10 that within the next twenty cars that pass, at least two passing cars will
have the same two-digit number as the last two digits in their license plates.
This may be any number from 00 to 99. Develop a program to simulate this
game to determine whether you should take the bet. Perform the simulation
100 times and determine the amount of money you would have won or lost.
Exercise 13.3
The random number generator implemented in most computers generates
what are known as pseudorandom numbers rather than true random numbers.
Footnote 1 in Section 10.2.5 describes an algorithm for such a pseudorandom
number generator. Here is a Scheme implementation of that algorithm:
(define random-maker
(lambda (m a seed)
(lambda (n)
(let ((u (/ seed m)))
(set! seed (modulo (* a seed) m)
(floor (* n u))))))
(define random
(random-maker (- (expt 2 31) 1) (expt 7 5) (random-time)))
If your implementation of Scheme has a procedure that gives you the time
of day, it should be used instead of random- time. If not, our naive definition
will suffice. Consider these two ways to test a random number generator.
Implement these two tests in Scheme, and run them using the random
number generator defined above. If your implementation of Scheme has a
random number generator, test it too. Interpret the results.
Our simulation concerns a gasoline station that has two lanes leading to self-
service pumps and two lanes leading to full-service pumps. This is not an
uncommon configuration in small stations. To run the simulation, we specify
the following initialization parameters:
• close-time: The closing time in hours assuming that the station opens
when the clock reads hours.
• prolit-sell: The profit that the station owner makes on each gallon of
self-service gas sold.
• profit-full: The profit that the station owner makes on each gallon of
full-service gas sold.
• pump-rate: The number of gallons of gasoline per minute that the pumps
deliver.
C\ closing time
arrival time
Output statistical
{
Figure 13.6 shows a flowchart or diagram of the gas station simulation. The
box on the top shows the gas station with four pumps. Moving downward, we
pass through various diamond-shaped boxes, which correspond to conditionals
or branches. Each branch follows a customer through the wait in the pump
queue until he or she is served and is dequeued and the purchase is recorded.
At closing time, those customers still in line are served but no new ones are
admitted. When all are served, the statistical data for the day are printed
out.
station
Tf^t^r%rt V service
wnicn serve pump
5lll — ^ITIT^fV*? pump
«iprvp V pump
pump7
customer
service
nuTnV>er~of counter
w
totalwait!
max— wait'
—— accumulator
accumulator
V t r*t Q 1 — T^fr*Ti 1
> accumulator
rpnort report procedure
munication between the objects, that is, the objects that send messages to
The gas station itself is an
other objects. object named station created by
station-maker (see Program 13.10). The object station responds, in ways
that will be described later, to four messages: report, which-serve, all-
empty?, and serve. Each pump in the gas station is an object created by the
procedure pump-maker (see Program 13.11). The pump object, which man-
ages its queue of customers who are waiting for service, responds to the four
messages: empty?, (referring to its queue of customers), enqueue!, size, and
check. The customers are objects created by the procedure customer-maker
(see Program 13.12). Each customer responds to the messages: gallons and
record. We also use objects created by the procedure service-maiker (see
Program 13.13). These objects, which are associated with each customer,
know the type of service (self or full) and the profit per gallon and use that
information to store information about the customer's purchase.
a counter that has initial value zero and update procedure addl. The object
arrival is a box that stores the arrival time of the next customer. Each
pump has a queue denoted by q and made by queue-meiker in Chapter 12.
The total amount of time a customer spends at the pump is kept by a box
object called timer. Finally, for each type of service (self and full), we keep
a record of the following four items: (1) A tally is kept of the total number
of customers in a counter object called number-of .
(2) The total waiting
time for all customers is kept in an accumulator object called total-wait.
(3) The maximum waiting time for all customers is kept in an accumulator
object called max-wait. (4) The total profit is kept in an accumulator object
called total-profit.
The values of the eight initialization parameters are passed to the pro-
cedure simulation-setupftrun, defined in Program 13.8, which starts the
simulation by invoking the procedure simulation with four arguments. The
first operand simulation invokes station-maker, which itself takes
peissed to
six arguments. The second and third operands passed to station-maker are
the two services that set up the mechanism for recording the data we want
to collect in our simulation. The rest of the code is clarified when we look at
the definitions of the object makers given below. The second operand passed
to simulation creates the clock.
given in Program 13.10. Looking at the code for simulation, we see that the
box aurrival stores the sum of the current clock time plus the time increment
until the next customer arrives, which is generated by the airrival-time-
generator. We enter loop and assume that it is not yet closing time and
that it is Then the station is passed
the arrival time of the next customer.
the two-part message in which the first part is which-serve and the second
part is a customer, created by invoking the procedure customer-maker (Pro-
gram 13.12) with the two arguments, the arrival time stored in arrival and
clock. From station-maker in Program 13.10, we see that the customer
so created is enqueued to the pump with the shortest queue that gives the
desired kind of service (full or self).
Next, station is sent the message serve. This causes each of the four
(define simulation-setuptrvm
(lambda (close-time 7,-self-service av-arr-time
profit-self profit-full
extra-timeJself-pump extra-timeCfull-pump pump-rate)
(let ((self-service (service-maker "Self" profit-self))
(full-service (service-maker "Full" profit-full)))
(simulation
(station-maker
'/,-self -service
self-service
full-service
extra-timeJself-pump
extra-time8full-pump
pump-rate)
(counter -maker addl)
av-eirr-time
(* 60 close-time)))))
pumps to check its queues. Looking at the code for pump-maker, we see that
when pump is passed the message check and the queue q associated with that
pump is not empty, the pump refers to its timer. Whenever a customer is
finished and dequeued, the timer is reset to -1. Thus if the timer is found to
store the value -1 when the message check is received, it updates the timer
to store the total time the customer at the front of the queue spends at the
pump. This total time includes both the actual pumping time and the average
extra time at the pump (one of the initial parameters). From then on, each
time pump receives the message check, the number stored in timer is reduced
by one, until the value stored is zero. When that happens and pump is passed
the message check, the customer has completed what had to be done at the
pump, and that customer is dequeued.
The dequeued customer is then passed a two-part message. The first part
is the method name record, and the second part is the variable service,
(define simulation
(lambda (station clock av-arr-time close-time)
(let ((arrival
(box-maker (+ (send clock 'show)
(arrival -time-generator av-eu:r-t ime) ) ) ) )
(letrec
((loop
(lambda ()
(loop)))))
2. the total waiting time of all customers who have passed through the queues
for that kind of service,
(define station-maker
(let ((check (lambda (p) (send p 'check)))
(all-empty? (andmap-c (lambda (p) (send p 'empty?))))
(shorter (lambda (pi p2)
(if (< (send pi 'size) (send p2 'size)) pi p2))))
(lambda (7,-self self-serv full-serv eitra-time-self extra-time-full pump-rate)
(let ((selfs (list (pump-maker extra-time-self pump-rate self-serv)
(pump-maker extra-time-self pump-rate self-serv)))
(fulls (list (pump-maker extra-time-full pump-rate full-serv)
(pump-maker extra-time-full pump-rate full-serv))))
(lambda msg
(case (1st msg)
((type) "station")
((report) (send self-serv 'report) (send full-serv 'report))
( (which-serve)
(let ( (piimp (apply shorter (if (< (random 100) '/.-self)
selfs
fulls))))
(send piimp 'enqueue! (2nd msg))))
((all-empty?) (and (all-empty? selfs) (all-empty? fulls)))
((serve) (for-each check selfs) (for-each check fulls))
(else (delegate base-object msg))))))))
When customer receives the method name record and the service object,
we see in the code for customer-maker in Program 13.12 that each of the
above statistics is updated with the information for the dequeued customer.
Then if the pump's queue is empty, its timer is reset to -1; otherwise it is
updated to show the total time at the pump for the customer who is now at
the front of the queue.
When clock shows that it is closing time, we enter the loop prepare-f or-
closing. If at least one of the queues is not empty, those customers in the
queues are served as above, but no new customers are enqueued. Finally,
when all queues are empty, the message report is passed to station. In
(define piunp-maker
(lambda (extra-time pump-rate service)
(let ((q (queue-maker)))
(let ((increment (lambda
(let ((gallons (send (send q 'front) 'gallons)))
(ceiling (+ extra-time (/ gallons pump-rate))))))
(timer (box-maker -1)))
(lambda msg
(case (1st msg)
((type) "pump")
((check) (if (not (send q 'empty?))
(let ((c (send timer 'show)))
(cond
((negative? c) (send timer 'update! (increment)))
((zero? c) (let ((customer (send q 'front)))
(send q 'dequeue!)
(send customer 'record service)
(if (send q 'empty?)
(send timer 'reset!)
(send timer 'update! (increment)))))
(else (send timer 'update!
(subl (send timer 'show))))))))
(else (delegate q msg))))))))
the code for station-maker (Program 13.10), we see that the message re-
port is sent to both objects self-serve and full-serv. In service-maker
(Program 13.13), the procedure report is invoked with the final values of
the statistics storedwhen each customer was dequeued. The procedure re-
port, defined in Program 13.14, displays this information. We shall run the
simulation for various parameters and see how this information is displayed.
We have taken a quick walk through the gais station simulation to illus-
trate the use of objects in asomewhat longer program than others we have
been studying. The program contains many interesting and subtle features
that are worth studying in detail until they are fully understood. The time
spent in coming to grips with each of the steps will contribute much to your
development as a programmer.
Let us now run the simulation. We call simulation-setup&run with the
following parameters:
(define custoaer-Baker
(lambda (eirrival-time clock)
(let ((gallons-puBped (gallons-generator)))
(lambda msg
(case (1st msg)
((type) "customer")
((gallons) gallons-pumped)
((record) (let ((service (2nd msg))
(wait (- (send clock 'show) arrival-time)))
(send service 'niamber-of !
close-time: 12 hours
'/.-self-service: 759c
av-arr-time 4 min.
profit-self So. 10 per gallon
profit-full So. 10 per gallon
extra-timeCself-pvimp: 5 min.
extra-timeCfull-pump: 8 min.
pump-rate: 4 gallons per min.
We then have:
[1] (simulation-setup&nin 12 75 4 . 1 . 1 5 8 4)
Self -Service:
The number of customers is 111
The average wait is 11
The maximum wait is 20
The total profit is 127.3
Full-Service:
The number of customers is 37
The average wait is 13
The maximum wait is 21
The total profit is 46.8
The program for our simulation does produce the information we want,
but it does not have a user-friendly interface in the sense that the user must
know what the parameters are and the order in which to put them when the
(define service-meiker
(lambda (full-or-self profit)
(let ((number-of (counter-maker addl))
(total-wait (accumulator-meiker +)
(msix-wait (accumulator-maker max))
(total -prof it (accumulator-meiker +)))
(leunbda msg
(case (Ist msg)
((type) "service")
((number-of!) (send number-of 'update!))
((total-wait!) (send total-wait 'update! (2nd msg)))
((max-wait!) (send max-wait 'update! (2nd msg)))
((total -prof it!)
(send total-profit 'update! (* profit (2nd msg))))
((report) (for-ef feet-only
(report full-or-self
(send number-of 'show)
(send total-wait 'show)
(send mcix-wait 'show)
(send total -prof it 'show))))
(else (delegate base-object msg)))))))
(define report
(Ijuabda (full-or-self num-cust total-wait meuc-wait profit)
(if (zero? num-cust)
(writeln " There were no " full-or-self "-Service customers.")
(begin
(writeln full-or-self "-Service:")
(writeln " The nvimber of customers is " niim-cust)
(writeln " The average wait is " (round (/ total-wait num-cust)))
(writeln " The meucimum wait is " msuc-wait)
(writeln " The total profit is " profit)))))
(define prompt-read
(lambda (prompt)
(display prompt)
(display " ")
(read)))
the program that will prompt the reader for the information needed to run
the simulation. For that purpose, we use the procedure prompt-read, which
prints its argument (the prompt) to the screen eind waits for a response to be
read from the keyboard. It then returns that response. The code for prompt-
read is in Program 13.15. An example illustrating the use of prompt-read
is
(define station-prompts
'("Elnter the number of hours the station is open:"
"Enter the percentage of self-service customers:"
"Enter the average time in minutes between arrivals:"
"Enter the profit per gallon from self-service customers:"
"Enter the profit per gallon from full-service ciistomers:"
"Enter the extra time at the pump for self-service customers:"
"Enter the extra time at the pump for full-service customers:"
"Enter the delivery rate of the pumps in gallons per minute:"))
(define gas-station-simulator
(letrec
((loop (lambda (Is)
(if (null? Is)
'()
[3] (gas-station-simulator)
Enter the number of hours the station is open: 12
Enter the percentage of self-service customers: 75
Enter the average time in minutes between arrivals: 4
Enter the profit per gallon from self-service customers: .10
Enter the profit per gallon from full-service customers: .10
Enter the extra time at the pump for self-service customers: 2
Enter the extra time at the pump for full-service customers: 4
Enter the delivery rate of the pumps in gallons per minute: 4
Self-Service:
The number of customers is 110
The average wait is 6
The mciximum wait is 12
The total profit is 120.70
Full-Service:
The number of customers is 35
The average wait is 8
The mciximum wait is 10
The total profit is 44.40
We can now use this simulation to see what happens when certain parame-
ters are changed. For example, ifwe keep all of the parameters fixed except the
average arrival time, we can see what happens when the time between arrivals
decreases. We kept the station open 12 hours, 75% of the customers selected
Self-Service:
The number of customers is 60 110 131 192 274
The average wait is 6 6 7 9 84
The maximum wait is 9 12 14 20 169
The total profit is 70.00 123.10 160.40 232.20 334.90
Full-Service:
The number of customers is 22 35 40 53 86
The average wait is 9 9 8 9 9
The majcimum wait is 11 11 12 17 18
The total profit is 28.10 44.40 45.70 66.10 100.40
Table 13.18 Gas station simulatiion with varying average arrival time
I
self-service, the profit per gallon on self-service sales was $0.10 and for full-
service sales was $0.10, the extra wait at the pump for self-service customers
was 2 minutes and for full-service customers was 4 minutes, and the pumps
delivered gasoline at the rate of 4 gallons per minute. The simulation was run
with the average time in minutes between the arrival of successive customers
taken to be 8, 4, 3, 2, and 1. The results are summarized in Table 13.18.
When the time between arrivals is large and long queues do not form at
the pumps, it is faster to use self-service. As the queues get longer with
more frequent arrivals, the smaller volume of full-service customers allows the
attendants to keep the waiting time relatively short, whereas the self-service
customers pile up and ultimately take hours to get out. Although a wait of
more than an hour is generally considered intolerable, there were times during
the oil crisis of 1978 when people did stay for hours in gas station queues which
extended for blocks around the station. In normal times, the station would
have to provide more lanes or lose the business, and a larger percentage of
customers would opt for full service.
In this simulation, after the data were entered, the program printed out a
summary of the day's business. It is possible to write simulations showing on
the screen a picture of the cars entering and advancing through the queues,
as well as the current values of the variables. There are many creative ways
in which simulations can be written using the tools developed in this text.
Exercise 13.4
Write a simulation of the following experiment. Two dice are thrown n times.
A record is kept of how many times each possible sum comes up. Since each
die is a cube with the numbers from 1 to 6 on its faces, the possible face sums
are the integers from 2 to 12. The output should be a table with the face sum
column and the percentage of times that sum came up in the second
in the first
column. Run the experiment with n taking the values 100, 200, 400, and 800.
To record the results of each toss, use 11 counters (as defined in Section 12.2.3),
one for each possible face sum, and increment the counter each time that
sum appears. Compare your results with the theoretical percentages, which
are 2.78%, 5.56%, 8.33%, 11.11%, 13.89%, 16.67%, 13.89%, 11.11%, 8.33%,
5.56%, and 2.78%. These are easily found by counting the number of ways a
given face sum can be obtained, dividing by 36 (the total number of ways two
dice can come up), and multiplying by 100. For example, a 7 can come up in
six ways, so ^ x 100% = 16.67%.
We can use simulation to compute the value of tt. For this we use the fact that
a circle of radius 1 has area equal to tt. We shall play the following game. We
throw a dart at a square board with sides of length 2 feet. The area of this
board is 4 square feet (see Figure 13.19). We assume that the dart is equally
likely to land at any point on the square. Thus the percentage of time it falls
Exercise 13.6
The definition of shorter station-maker does not handle stations with
in
fewer than or more than two pumps of the same variety. Rewrite shorter
and any other procedures so that the simulation supports arbitrary numbers
of pumps of these two varieties.
Exercise 13.7
Modify the gas station simulation to apply to a case where there are three
lanes dedicated to self-service and only one lane dedicated to full service. Run
the same input data as in the simulation runs given above and compare the
results you get with the data in the tables.
Exercise 13.8
The gas station simulation does not generalize as simply as we might hope.
For example, what if the station also wanted information on their diesel fuel?
Diesel fuel has a different pump speed, but that would not be any cause to
make significant changes in the program. You would need to make a small
alteration to the argument simulation-setupftnm and a few other
list of
minor changes. However, by adding diesel fuel, you would have to add much
to station-maker. Make those changes, and test your program. Next, add
corn fuel in a similar fashion. Go back to the program and build an object or
apply abstraction principles so that if the station needs to support yet another
kind of fuel, the task will be simple.
(define prompt-read
(lambda items
(for-each display items)
(display " ")
(read)))
Can this definition be used in place of Program 13.15? How is it better than
Program 13.15?
but it does not echo that data back to be sure that the correct data was
entered. For example, when the response to the prompt "Enter the number
of hours the station is open:" is 12, the echo printed on the screen could
say "The station is open 12 hours." Rewrite the definition of gas-station-
simulator so that it provides an appropriate echo for the response to each
prompt.
Exercise 13.11
Add a report figure for the longest line that appeared at each pump.
Exercise 13.12
Modify report so that dollar figures are printed with two decimal places.
Exercise 13.13
Explain why the following definition of gas-station-simulator can not be
used in place of Program 13.16:
(define gas-station-simulator
(lambda ()
Let us think back to the introduction to Part 1. The restaurant where you
dine is a little cafe in Paris. Ordering may not be simple; you might not
be able to read the menu; it is in French. If that were the case, however,
you would probably be carrying with you a dictionary, which is analogous to
our syntax table, for looking up all the unfamiliar words. Here's one from
the menu: Poulet rati. First you look up the meaning of poulet, and it is
"chicken," and then you look up roti and it is "baked." So you know that it
is "baked chicken." The whole menu is full of such unfamiliar French words.
However, with your dictionary, you can look up each word and translate the
entire menu into your native language before placing your order. Derived
special forms are like words in a foreign language. When you do not know
what they mean, you look them up in a derived special forms dictionary.
For example, let expressions can be translated into applications of lambda
expressions. Applications and lambda expressions correspond to words in
your native tongue.
Suppose that you do not have a dictionary but your Parisian waiter and
you know a bit of German. You ask him what poulet roti is, and he says that
he does not know how to translate poulet roti into your native tongue, but he
can say German. He says ^^Backhuhn," and you translate it to "baked
it in
chicken." The French is translated into German, and the German is translated
into something you understand. This also happens with derived special forms.
For example, let* expressions are first translated into let expressions, and then
the let expressions are translated into familiar words: lambda expressions
and applications. Chapter 14 presents two different mechanisms for inserting
derived special forms into the syntsix table. This allows us to extend our
vocabulary of expression types. Delay expressions, introduced in this chapter,
These lists are not really infinite, but there is no way to show they are not.
Most of the finite list-processing procedures can be recast, with infinite lists
replacing finite lists. In fact, by removing the tests for the empty list, the task
is virtually complete. Working with infinite lists requires thinking about lists
that never end. You have already seen circular lists, which also never end, but
those lists always repeat their values. With infinite lists, different values can
appear in every position. Consider an infinite list of the positive integers (1
and consing onto it would form the nonnegative integers. All that remains
is to convince you that such a list does exist.
14.1 Overview
(.operator operand . . . )
When such an application is made, the operator and the operands are evalu-
ated in an unspecified order, ^ and then the procedure (which is the value of the
operator) is applied to the arguments (which are the values of the operands).
We have also encountered several special forms in which the subexpressions
following the keyword are treated differently from the operands of a proce-
dure. Examples of these are and, begin, cond, case, define, if, lambda,
let, let*, letrec, or and set!, each with a syntax of its own. Some of
these, like let, have been introduced to make it easier to read programs, for
any program using let could be rewritten using an application of a lambda
expression in place of each let expression. Such keywords are referred to as
derived keywords. One of the convenient features of Scheme is that it is an
extensible language that allows the user to add new special forms to make the
language more convenient to use and to provide a mechanism to do tasks that
procedures cannot perform. We shall study two mechanisms for making such
additions in this chapter.
^ Programs that rely on an order of evaluation are said to be ill formed. Since the order
of evaluation implementation dependent, such programs are not portable, and they can
is
In this book we have used several special forms without defining them as
procedures. In fact, it is the nature of these forms that they cannot (or
should not) be defined as procedures either because some of their operands
are not to be evaluated or because the order of evaluation of their operands is
(define sm (+ 3 4))
The body of a lambda expression is not evaluated until that lambda expres-
sion is applied to its arguments, and since the thunk (lambda () (+3 4))
has no arguments, it is invoked by merely enclosing the lambda expression in
parentheses. Since the thunk in this case is bound to the variable sm, we can
invoke it by enclosing sm in parentheses, that is, by writing (sm). We are thus
able to postpone the evaluation of an expression until we need it by making it
into a thunk and binding a variable to that thunk. It would be nice to have a
procedure freeze that, when applied to an operand, has the effect of forming
a thunk that has that operand as its body. Suppose we write:
(define freeze
(lambda (expr)
(lambda () expr)))
But when the define expression is evaluated, before being bound to sm, the
expression (freeze (+ 3 4)) is evaluated. Since freeze is a procedure,
its operand (+ 3 4) is evaluated. Thus we defeated the purpose for which
we wrote the procedure freeze, which was to postpone the evaluation of its
operand until sm is called. What happened is that (+ 3 4) is evaluated during
the definition of sm instead of when sm is called. Thus freeze cannot be a
procedure; it has to be the keyword of a special form if it is to accomplish
what we want.
To declare this special form with keyword freeze, we make use of a special
form with keyword macro. We ^ would like freeze to have the syntax (freeze
expr) and to transform into the thunk (lambda () expr) without evaluating
^ At the time this book is being written, the Scheme community has not yet agreed upon a
standard way of declaring specied forms. In this book, we use two methods that have been
(lambda () expr)
sion into the program instead of the macrocode. Thus the subexpression expr
of the special form (freeze expr) was not evaluated when the procedure (or
thunk) was created by evaluating (lambda () expr).
How is the macroexpansion accomplished? We have to write a procedure
that literally transforms the macrocode into the macroexpansion of that code.
Let us call that procedure f reeze-transf orraer; it takes the macrocode code
as its argument and returns the code for the macroexpansion. In our case, the
(define freeze-transfomer
(lambda (code)
(make-lambda-ezpression '() (list (2nd code)))))
(define make-lambda-ezpression
(lambda (parameters body-expressions)
(cons 'lambda (cons peirameters body-expressions))))
included in some implementations. These methods use special forms with keywords macro
and ertend-syntax. If these are not implemented in the version you are using, read the
manual for your implementation to see how it declares special forms, and use that method
instead. In general, until a standard is agreed upon, code including user-made special forms
is not portable.
by the body of its lambda expression with its parameters replaced by the
arguments to which they are bound to get:
14.3 Macros
In general, the special form with keyword macro has the syntax
where name is the keyword of the new special form being declared and
transformer is a procedure of one argument that takes the macrocode and
returns the macroexpansion. In our example above, freeze is the keyword,
and
(lambda (code)
(cons 'lambda (cons '
() (list (2nd code)))))
where the ellipsis (three dots) means that there is a finite number of expres-
sions following the word freeze and that there is at least one such expression.*
In any actual case, the macrocode is a list that starts with the keyword
freeze and always has at least one expression following it. If we represent
this macrocode by the variable code again, then (cdr code) is just a list of
the expressions that make up the body of the lambda expression into which the
macrocode is expanded. The f reeze-transf ormer procedure defined above
can be modified so that it produces the right macroexpansion for this version
of freeze:
It would be convenient if Scheme were to have a way of taking the two sides
of the syntax table entry and declare the special form for us. In essence, the
system would be writing the transform procedure for us and using it to declare
the macro. Such a special form, called extend-syntax,^ was developed (see
^ Here is a way to get macro if you have ext end-syntax in your implementation of Scheme:
(extend-syntax (macro)
((macro name transformer)
(let {(t transformer))
(extend-syntax (name)
(x ((with ((h 'with)) w) ((v (t 'x))) v))))))
where macro-pattern and expansion-pattern are the left and right sides, re-
(extend-syntax (freeze)
((freeze exprl expr2 ...) (lambda exprl expr2 ...)))
Since no standard way of making special forms has been agreed upon, we shall
demonstrate both ways of doing it — that is, using macro and extend-syntax
in the rest of this chapter.
Along with the macro freeze, there is the procedure thaw, which invokes a
frozen entity (a thunk) and returns its value. The procedure thaw is defined
as follows:
(define thav
(lambda (thunk)
(thunk)))
(define th (freeze (display "A random number is: ") (ramdom 10)))
Each time the thunk is thawed, the expressions are reevaluated. Thus each
time we thawed the thunk th in the example, another random number is
is called and the result obtained is the same, in the absence of side effects. We
propose to evaluate the postponed expression only the first time it is called
and on subsequent calls to return the already computed value. We declare
the special form delay to postpone the evaluation by creating a promise, and
a corresponding procedure force to evaluate (or "force") the promise. When
the promise is forced for the first time, the value of the postponed expres-
sion computed and returned. Each succeeding time the promise is forced,
is
the same value that was computed the first time is returned. Consider the
following:
(define pr (delay (display "A random number is: ") (random 10)))
(define delay-transformer
(lambda (code)
(list 'make-promise (cons 'freeze (cdr code)))))
(extend-syntax (let)
((let ((vaur val) ...) expri expr2 ...)
(danbda (var ...) expri expr2 ...) val ...)))
code represents the macrocode, then the list of parameters is built up by first
taking the (2nd code) to get a list of pairs of var' s and val's. We extract the
list of var's by taking the 1st of each pair in the list using map as follows:
^ When using user- declaired macros that have the same keywords cis special forms in Scheme,
you might want to avoid collisions with the built-in forms. We suggest that you siuround
the keywords of those you declaire with equed signs; e.g., =let= in place of let.
Similarly, we can build the list of operands from the macrocode by taking the
2nd of each pair. This leads to:
(define Beike-list-of-operands
(lambda (code)
(nap 2nd (2nd code))))
A list of the items in the body of the lambda expression we are building is
(define Bake-list-of-body-items
(lajBbda (code)
(cddr code)))
With these helping procedures, we can write the transform procedure and
declare it as the macro for let.
This is really only half of the declaration of the macro let since there is also
the so-callednamed let, which has a different syntax. We shall return to
the named let in the exercises, where we rely on the following discussion of
letrec. The above version of the macro declaration of let using the special
form with keyword macro clearly illustrates the advantage of using extend-
syntajc to declare a macro. Exercise 14.6 at the end of this section suggests
some interesting modifications to let so that it displays appropriate messages
when an expression with keyword let is entered with an incorrect syntax. For
example, if we write (let ( (a 3) ) ) incorrect syntax should be signaled since
,
a let expression must contain at least one subexpression following the binding
pairs. If we use macro to declare our special forms, we must explicitly include
On the other hand, one of the great advantages of using ext end-syntax is
that it has built-in syntax checking, so we do not have to include our own tests
the expression val . . . whose value will be bound to var . . . cannot contain
var . . . recursively, for looking at the pattern for the macroexpansion,
in val . . . refers to an outer scope. The special form letrec does allow for a
recursive scope.
The macro letrec has the syntax table entry:
macroexpansion. This allows the use of recursion in var. Let us now write the
macro for letrec. Again, it is a simple matter to do so using ext end- syntax.
Consider the definition of the procedure odd?, which is defined using a letrec
expression:
(define odd?
(let ((even? "any")
(odd? "any"))
(begin
(set! even? (lambda (n) (if (zero? n) #t (odd? (subl n)))))
(set! odd? (lambda (n) (if (zero? n) #f (even? (subl n))))))
odd?))
Let us next look at how to declare letrec using macro. We first consider
how we construct the pairs of the form (var "ajiy"), which are in the let
expressions of the macroexpansion. After we get the var's from the 2nd of
the macrocode, we use map to give us the desired pairs of the form (var
"any"). Similarly, we build the set! expressions, and finally, we build a list
of expressions that complete thebody of the let expression. This leads to the
declaration of letrec using macro that is given in Program 14.10,
(macro letrec
(lambda (code)
(cons 'let
(cons (map (lambda (z) (list (1st z) "any")) (2nd code))
( append
(map (lambda (z) (cons 'set! z)) (2nd code))
(cddr code))))))
Something you usually want to avoid is the creation of infinite loops. How-
ever, as an interesting demonstration of the use of letrec, we shall write
a special form cycle that takes an arbitrary number of subexpressions and
runs each subexpression in succession and then starts over again, repeating
this loop indefinitely. The syntax table entry for cycle is
(define cycle-proc
(lambda (th)
(letrec ((loop (lambda
(thaw th)
(loop))))
(loop))))
and be sure that division by zero does not occur because the second subex-
pression is not evaluated if x is zero. Thus we want or to be a macro that
can take any number of subexpressions, including no subexpressions. If or is
called with no subexpressions, it returns false. Having taken care of the case
of no subexpressions, we consider the following syntax table entry for or with
several subexpressions:
This works because if first evaluates ei and if it is true, it returns the value of
ei in the consequent. If ei is false, it skips to the alternative and returns the
"recursive" value obtained for the alternative. This looks like recursion, but
we must remember that these or expressions are not being evaluated. Rather
they are macrocode, which is being transformed into if expressions that are
the macroexpansions. We have treated the case of (or e), which should have
the same value as e, because using the syntax table entry, (or e) expands to
(if e e (or)) and (or) expands to #f.
We could use the above macroexpansion for or, but it does not work effi-
(or ex 62 ...) = (let ((val ei)) (if val val (or 63 ...)))
We expect this to return #t. However, when the program is entered, the or
expression is expanded into
and the value returned is #f because the leist val has been captured within
the scope of the nearest binding, and unfortunately the variable val was also
used in the let expression in the declaration of the macro or. There are several
ways of avoiding this capturing. We shall make use of the fact that when a
frozen entity is thawed, it is evaluated in the environment that was in effect
when the entity was frozen. We first define a procedure, called or-proc, which
takes a list of thunks as its operand. Then to declare the macro or, we freeze
the operands and pass them to the procedure or-proc. Here is the definition
of or-proc:
(define or-proc
(lambda (th-lis.t)
(cond
((null? th- list) «f)
(else (let ((v (thaw (cai th-lis t))))
(if V V (or- Droc (cdr th -list))))))))
How are the cases of zero expressions and one expression handled by this
Program 14.13 or
We can also use extend-syntax to declare the macro or based on the above
syntax table entry. We have:
Program 14.14 or
(extend-syntax (or)
((or e ...) (or-proc (list (freeze e) ...))))
Several more special forms are developed in the exercises. The ability to
write your own Scheme is a powerful tool that can be used to
special forms in
make programs more readable. Most important, it allows you to build your
own textual abstractions. In the next chapter, we shall make use of the special
form delay to develop the idea of streams or "infinite lists."
Exercises
Exercise 14-1
What is the output of
What general statement can you conclude from these examples concerning the
output when a transform procedure is applied to the quoted macrocode? Some
implementations of Scheme have a procedure called expand, which converts
the quoted macrocode into its macroexpansion.
Exercise 14-2
Declare the letrec macro using ext end- sjrnt ax without using let in its macro-
expansion.
Exercise 14-3
Consider the declaration of the macro or, below. Does this declaration suffer
the variable capturing that we were able to avoid using or-proc and a list of
thunks?
(extend-syntai (or)
((or) «f)
((or e) e)
((or el e2 ...) (let ((val el) (th (freeze (or e2 ...))))
(if val val (thaw th) ) ) )
subexpressions are true, it evaluates to the last one; otherwise it is false. Test
your macro on:
(and)
(and «t)
(and «f)
(and #t #t »t)
(and #t »t #f)
Note that the capturing problem need not arise in declaring and.
evaluates to 3 because the x in the binding pair (y x) must look up its value
in an environment other than the local environment of the expression
The value 3 is found since that let expression is nested within the let expression
with binding pair (x 3). If we had wanted the x in (y x) to refer to the x in
(x 10), we would have had to put the (y x) in another nested let expression,
as follows:
(let ((uari vali) (.var2 vo/2) (vars vala)) expri expr2 ...)
instances of vari in val^ and instances of var^ or var2 in vals cannot refer
to vari or var2 in this let expression but must find their values in a nonlocal
environment. However, if we were to write nested let expressions, such as
then instances of vari in va/2 can refer to the vari in the first binding pair, and
instances of vari or var2 in vals can refer to the f ari or i'ar2 of the preceding
two binding pairs. We used the Scheme special form let* in Section 10.2.5.
It has a syntax similar to that of let but behaves as though the successive
binding pairs are in nested let expressions. In fact, if there is only one such
binding pair, then let* is the same as let, so that
(let ((rari vali)) (let* ((i;ar2 val2) .-) expri expr2 ...))
Exercise I4.6
The procedure let-transformer is correct only if the user obeys let's syntax.
The special form let expects a list of n 2 elements. The first must be the
-f-
symbol let; the second must be a list of pairs where each pair is a list of two
elements, in which the first element must be a symbol. The remaining n >
elements can be arbitrary expressions. Here are some incorrect examples:
(let (d 3) (y 4)))
(let ((3 3) (y 4)) (* x y))
(let (d 3) (y 4 5)) (* i y))
(let X 3 (* I y))
(let (("i" 3) (y 4)) (* "i" y))
Exercise 14.7
The error information from the previous exercise does not pinpoint exactly
where the error occurred. Redesign the information displayed so that you can
better determine where the error occurred.
((letrec
(,inam,e (lambda (.var ...)
expri expr2 . . ))
name)
val . . . )
(if (positive? n)
(begin
(set! sum (+ sum n))
(set ! n (subl n)
(loop))))))
(loop)
sum))
We would like to introduce the special form while, which allows us to write
the above program eis:
using repeat that models the test program of the previous exercise.
The for expression is used for modeling iteration. The variable var is initial-
is evaluated. Finally, var is reset to the evaluation of step, and the process
repeats.
Define for-traLnsformer or declare for using extend-syntax given the
syntax table entry below.
This solution is subtle because each of step, test, and expri expr2 will be . . .
using var. For example, a typical use of for expressions is to add the elements
of a vector:
(define vector-siim
(lambda (v)
(let ((n (vector-length v))
(sum 0))
(for i (addl i) (= i n) (set! sum (+ sum (vector-ref v i))))
sum)))
Exercise 14.13: do
The special form do has the syntax table entry:
((letrec
((loop (lambda (.var ...)
(cond
{test exiti exit2 )
(else (begin expri expr2 . •)
(loop step ...))))))
loop)
initial . . . )
The variable loop must not be among var . . . and it must not be free in test,
(beginO e) = e
beginO evaluates its subexpressions in order and returns the result of evalu-
ating the first one. Define the procedure beginO-proc, which always takes
exactly two arguments. Why is the syntax table entry
(beginO expri expr2 ...) ^ ((l£unbda args (car args)) expri expr2 )
incorrect? [Hint: Read the specification carefully. What can we say about
the order of evaluation of operands?) Test beginO-proc by defining beginO-
transf ormer or declaring beginO using extend-syntax.
Redefine member-trace and factorial below, using just the syntax table
entry for cond expressions.
(define factorial
(lambda (n)
(cond
((zero? n) 1)
(else (
n (factorial (subl n)))))))
case, it includes the symbol else. Fill in the rest of the declaration of cond
below. (See the previous exercise.)
Exercise 14.19
If we did not have variable-case from the previous exercise, then the case
example above would require an additional evaluation of (remainder 35 10).
Instead, we can choose a variable, say target, that will always hold the value
of the first operand of the most deeply nested case expression. Given this
Design a special form object-maker that abstracts this pattern of use. Are
there other patterns of use with object makers that can be abstracted?
15.1 Overview
In Chapter 14, we discussed the special form with keyword delay, which is
"memoizing" the body, as well as delaying its evaluation. We shall now see
how we use this "lazy evaluation" to handle infinite lists.
Suppose that our work requires that we process a list of random numbers,
but we are not sure how long the list has to be. We can choose a very large
number and make a list that long, each member of the list being a random
number, say, between 2 and 12, inclusively. Thus, the list can be generated
by
(define random-2-to-12-list
(lambda (n)
(if (zero? n)
'()
printed along with the number of integers summed. The following program
does this:
built using such conses a delayed list. We illustrate this method of producing
a delayed list by constructing del-list containing the two elements (fib 8)
and (fib 9):
(define del-list
(cons (fib 8)
(delay (cons (fib 9)
(delay '())))))
In order to look at the first element of del-list, we take the car. Our goal
now is to define operators on the data type delayed lists.The first of these is
delayed-list-car, which is the same as car, since we do not delay the first
argument to cons. Thus
(define delayed-list-car
(lambda (x)
(car x)))
second element in del-list, we must first take the cdr of del-list, yielding
a promise, and then force that promise. The result is another delayed list:
Because of the memoizing effect of delay, (fib 9) is evaluated the first time
the above call is made, and the value 34 is stored. The next time the call
is made, 34 is returned without reevaluating the (fib 9) in (cons (fib 9)
(delay '())).
If we apply delayed-list-cdr to del-list and then apply delayed-list-
cdr to that result, we get the list (), which we call the-null-delayed-list.
'
defined as
(define delayed-list-null?
(lambda (delayed-list)
(null? delayed-list)))
ate their operands before passing their values to the procedure defeats the
purpose of the delay. Thus, we must declare delayed-list-cons as a special
form using the following syntax table entry:
(define del-list
(delayed-list-cons
(fib 8)
(delayed-list-cons
(fib 9)
the-null-delayed-list) )
so that building delayed lists looks analogous to building lists. We can also
rewrite the definition of r£aidom-2-to-12-list using our new constructor
delayed-list-cons to give us random-delayed-list:
(define random-delayed-list
(lambda (n)
(if (zero? n)
the-null-delayed-list
(delayed-list-cons
(+ 2 (random 11))
(random-delayed-list (subl n))))))
The output from this procedure has the same form as that of our previous
version, but now a random number is computed only when it is used.
In order to see the elements of a delayed list, it is convenient to have a
procedure delayed-list->list, which converts a delayed list delayed-list
into a list of its elements:
(define delayed-list->list
(lEunbda (delayed-list)
(if (delayed-list-null? delayed-list)
'()
We can now use this to look at the elements in the delayed list (random-
delayed-list 20):
Exercise 15.1
Define the delayed list consisting of the first n even integers starting with 0.
Define the delayed list consisting of the first n odd integers starting with 1.
Exercise 15.4
Delayed lists can be treated as an abstract data type with a few basic opera-
tors. Convince yourself that if the definitions of the five entities are given, all
• the-null-delayed-list,
• delayed-list-null?,
• delayed-list-car,
• delayed-list-cdr,
• delayed-list-cons.
One alternative way of defining these entities is based on the decision to delay
the car part as well as the cdr part of a cons cell. Thus, the syntax table entry
for delayed-list-cons becomes:
How must the other four basic definitions of Program 15.3 be changed? Does
the behavior of any of the procedures defined in this section change using
these definitions of the five basic entities? Discuss the behavior of del-list
15.3 Streams
still remains, and hence the variable n and the terminating condition may be
omitted from the definition of random-delayed-list.
When the terminating condition is omitted in the definition of a delayed
or with
(macro stream-cons
(lambda (code)
(if (not (= (length code) 3))
(error "stream-cons: Wrong number of expressions" code)
(list 'cons (2nd code) (list 'delay (3rd code))))))
(define random-stream-generator
(lambda ()
and
(define the-null-stream
(stream-cons the-end-of -stream-tag the-null-stream))
(define list->stream
(lambda (Is)
(if (null? Is)
the-null-stream
(stream-cons (car Is) (list->stream (cdr Is))))))
(define end-of-stream?
(lambda (z)
(eq? X the-end-of-stream-tag)))
For example,
A stream is called a finite stream if it has the property that from some element
on it becomes the-null-stream. Any noncircular list that is converted into
a stream is an example of a finite stream. We use the term infinite stream to
refer to those when such a distinction is called for.
streams that are not finite
gument is the-null-stream.
(define streani->list
(lambda (strm n)
(if (or (zero? n) (stream-null? strm))
'()
(define f inite-stream->list
(lambda (f inite-strm)
(stream->list f inite-strm -1)))
Other streams can be defined using stream-cons. For example, the stream
of positive integers can be defined as:
(define positive-integers
(letrec
( (stream-builder
(lambda (x)
(stream-cons x (streaim-builder (addl x))))))
(stream-builder 1 ) )
(define even-positive-integers
(letrec
( (stre«un-builder
(lambda (x)
(stream-cons x (stream-builder (+ x 2))))))
(stream-builder 2)))
(define powers-of-2
(letrec
( (stream-builder
(lambda (x)
(streeim-cons x (stream-builder (* x 2))))))
(stream-builder 1)))
The definitions of these three streams share common features that lead us to
think about abstraction. We build-stream that abstracts
define a procedure
the structure of the definitions of these streams. The first place in which the
three differ is in the initial value of the argument x. We call this initial value
seed. The other place where they differ is in the procedure in the last line,
which appears as the operand to the local procedure. This procedure is the
(define build-strean
(leonbda (seed proc)
(letrec
( (strejuB-builder
(lanbda (x)
(stream-cons x (strezun-builder (proc x))))))
(stream-builder seed) ) )
rule for going from the current value of x to the next value of x. We call this
(define even-positive-integers
(build-stream 2 (lambda (x) (+ x 2))))
(define powers-of-2
(build-stream 1 (lambda (x) (* x 2))))
and the stream of random numbers defined above can be defined using build-
stream if the seed is (+ 2 (random 11)) and the transition procedure is
(define random-streeun-generator
(lambda
(build-stream (+ 2 (random 11)) (lambda (x) (+ 2 (random 11))))))
With a slight modification of the above technique, we can define the stream
of factorials. To do so, we define a local procedure stream-builder with two
parameters. We have:
(define factorials
(letrec
( (stream-builder
(lambda (x n)
(stream-cons x (stream-builder (* x n) (addl n))))))
(stream-builder 1 1)))
Certain of the procedures that were defined in the previous section for de-
layed lists can be redefined for streams. For example, we can redefine the
procedure delayed-list-sum of Exercise 15.3 to get the procedure stream-
sum, which sums the first k terms of a stream of numbers. If a stream of
numbers is a finite stream, then by putting in the appropriate test for the-
null-stream, we can write sura-finite-stream, which sums num- all of the
bers in the stream preceding the-null-stream. If the stream is infinite, we
cannot ask for the sum of all of the elements of the stream. Similarly, we
cannot append one infinite stream onto another, since the first stream has no
end. Always be sure operations will terminate before applying them to infinite
streams.
From a given stream strm, we can build a new stream in which a given
procedure proc is applied to each element of strm. The procedure stream-
map, which builds this new stream, is defined by
This enables us to define the infinite stream of odd positive integers as shown
in Program 15.19.
Now let strml and strm2 be two infinite streams, and let an be the nth
element of strml and let bn be the nth element of strra2. If proc is a procedure
that takes two arguments such that (proc a^ bn) is defined, a stream can be
built that has the element (proc a^ b^) as its nth element. The procedure
that applies proc to the corresponding elements of the two infinite streams
to form the new stream stream-apply-to-both and is defined in
is called
Program 15.20. This enables us to define stream-plus and stream-times
as the streams obtained by taking the element-wise sum and element-wise
product of two streams of numbers. (See Program 15.21.)
(define stream-apply-to-both
(lambda (proc)
(letrec
( (str-app
(lambda (si s2)
(stream-cons
(proc (streeun-car si) (stream-car s2))
(str-app (stream-cdr si) (stream-cdr s2))))))
str-app) )
(define odd-positive-integers
((stream-filter-out even?)
positive-integers)
(define positive-integers
(stream-cons 1 (stresun-map addl positive-integers)))
1 2 3 4 5 6 7 ...
multiply
1 2 6 24 120 720 5040
(define factorials
(stream-cons 1 (stream-times factorials positive-integers)))
112 3 5 8 13 ...
112 3 5 8 13 21 ...
add
1 2 3 5 8 13 21 34 ...
(define f ibonacci-numbers
(stre<un-cons
(streaun-cons 1
(stream-plus
f ibonacci-numbers
(strecun-cdr f ibonacci-numbers))))
A prime number is a number, other than 1, that has only 1 and itself as
factors. Thus 2, 3, 5, 7, 11, and 13 are the first six primes. Eratosthenes, who
lived in the third century B.C., devised a clever way of finding all of the primes
up to some given number N. First, write a list of all of the integers from 2
up to N. Then, with 2 eis the base, remove all multiples of the bcise that are
greater than the base. Now take the first remaining number after the base (in
this case, 3) and call it the base. Once again, remove all multiples of the base
greater than the base. We continue this process, choosing as the new base the
first remaining number that follows the preceding base, and then removing all
multiples of the new base that are greater than the new base, until there are
(define divides-by
(leunbda (n)
(lambda (k)
(zero? (remainder k ci)))))
(define prime-numbers
(letrec
((primes
(lambda (s)
(stream-cons
(stream-car s)
(primes ((sieve (stream-car s)) (stream--cdr s)))))))
(primes (stream-cdr pos itive- integers)))) -
no more numbers to take as the base. The remaining numbers are the primes
less than or equal to N. This method is called the Sieve of Eratosthenes. It is
used below to find all of the primes up to 20. Each successive list is the result
of removing multiples of the next base.
(2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20)
(23579
(2357
11
11
13
13
15 17
17
19)
19)
and are greater than the base. Such a filter, which we call sieve, and the
stream prime-numbers are defined in Program 15.26.
In Program 15.27 we show another way of defining the stream of all prime
numbers that combines the ideas of Exercise 7.20 and the data recursion used
in Program 15.23. Instead of testing with all odd integers < t/n, we now can
restrict our testing to the odd prime numbers < -y/n. Assuming we have the
(define has-prime-divisor?
(lajnbda (n)
(let ( (max-value (sqrt n) )
(letrec
((try (lambda (primes)
(and (<= (stream-car primes) max-value)
(or (zero? (remainder n (streeun-car primes)))
(try
(stream-cdr primes)))))))
(try odd-primes)))))
(define odd-primes-builder
(lambda (n)
(if (has-prime-divisor? n)
(odd-primes-builder (+ n 2))
(stream-cons n (odd-primes-builder (+ n 2))))))
Exercises
those numbers having the same sum listed in order of increasing numerator
a. Those fractions that are not in lowest terms are omitted from the enu-
meration. Thus the enumeration begins with 1/1, 1/2, 2/1, 1/3, 3/1,...
Define a stream positive-rationals that contains all of the positive ratio-
nal numbers with numerator and denominator having no common divisors
greater than 1. Represent the rational number a/h as a pair (list a b) and
use the Scheme procedure gcd to test whether a/h is in lowest terms. Test
your program by listing the first 20 elements of the stream.
(i 1) (i - 1 2) (i - 2 3) . . . (2 2 - 1) (1 i)
stream containing as its first i elements the zth diagonal, followed by the-
null-stream. Test it with 4 and 5.
(define stream-append
(lambda (finite-stream stream)
(cond
((stream-null? finite-stream) stream)
(else (stream-cons
(define int-pairs-generator
(lambda (i)
(stream- append (diagonal i) (int-pairs-generator (addl i)))))
Exercise 15.15
The problem with stream-append mentioned in the previous exercise can
be corrected by treating stream-append as a syntactic extension with the
following syntax table entry:
For example:
(chcur->integer #\A) ^^ 65
(char-> integer «\B) =» 66
(cheur->integer #\a) =» 97
(char->integer «\b) ^ 98
(cheir->integer #\0) ^^ 48
(char-> integer «\1) ^ 49
Scheme also has the procedure integer->ch2Lr, which is the inverse of char-
>integer; that is, if n is any number between and 127, inclusive, then
(integer->chsu: n) returns the character corresponding to n, and we again
use the ASCII code to determine that character. For example,
Special Scheme characters are used to denote some of the control characters
on the computer keyboard. For example, a blank space, which corresponds to
pressing the space bar on the computer keyboard, is denoted by the character
#\space, and a newline (or line feed) is denoted by the character tXnewline.
Some implementations of Scheme also contain the character #\retum, which
produces the control character corresponding to pressing the return or enter
key on the computer keyboard.
There are also a number of predicates that are used to test the order of two
characters by comparing the order of their ASCII codes: ch2a'=?, cliar<?,
ch.ar>?, ch.eoc<-?, and char>=?. We have:
gument is an uppercase alphabetic character. Along with these are the two
predicates char-upper-case? and char-lower-case? which test the case of
a letter:
There are some string procedures that also make use of characters. For
example, string->list is a procedure that takes a string and returns a list
Exercises
fiuiction). This method has the disadvantage that some letters are used
more frequently than others to start words, so the buckets would not be filled
uniformly. A better hash function is one that uses the sum of theASCII
codes of all the characters in the string modulo some fixed number m. This
will tend to distribute the words more evenly through the m buckets. Define
the procedure hash-function, which has as its parameter an integer m and
returns another procedure, which, when passed a string, returns the sum of
the ASCII codes of the first n characters in the string modulo m (that is, the
remainder when the sum is divided by m). First, define hash-function so that
it is case sensitive, and then redefine it with the name hash-f unction-ci so
that it is case insensitive and treats all letters as if they were lowercase. Test
your procedures on:
15.5 Files
returns an input port associated with the file named "inputl.dat". The
port returned is capable of delivering characters from the file "inputl.dat".
The procedure read takes a port as an optional argument, and if that argu-
ment is present, it reads from that port. For example, suppose that the file
Then we assign the port that we defined above to the variable port-in and
see how read successively reads each item:
When the reading of the file is finished, we invoke close-input -port, which
has the effect of closing the input port so that no further operations can be
performed on it. Since many computer operating systems limit the number of
ports that can be opened at the same time, it is good practice to close ports
100
150
200
250
returns the sum of the numbers in the file "input2.dat", namely 700.
In addition to the procedure read, which reads the next item. Scheme
provides the procedure read-cheur, which reads the next character. Scheme
writes that character using the #\-notation for characters. For example, the
character A is written as #\A. If read-cheo: is called with no argument, it
reads the next character from standard-input. If it has one argument, that
argument must be a port, and it reads the next character from that port.
Thus if a file "input3.dat" contains
Testing 12 3
(#\T #\e #\s #\t #\i #\n #\g #\space #\1 #\space #\2 #\space #\3)
To write directly to a file, we must first associate an output port with that
file. This is done with the procedure open- output -file, which takes as its
argument a string that identifies the file to which the output should be sent.
( . '
returns a port associated with the file "output.dat". The port returned
is capable of writing characters to the file "output.dat". If the file does
not already exist, it creates the file "output.dat". If the file does exist,
the behavior depends upon the particular implementation of Scheme you are
using.
We have used write and display as procedures of one argument. They
printed their argument on standard- output, which has been the computer's
video display. Both of these procedures accept a port as an optional second
argument. When a port is present as its second argument, the procedure
sends a printed representation of its first argument to that port. Similarly,
newline can take a port as an optional argument. Here is an example:
to the file "output .dat". As was the case with input ports, it is good practice
to close the output port when one is finished. This is done with the proce-
dure close-output-port, which takes a port as its argument. Had we used
write instead of display, the sentence would have been printed in the file
' For example, PC-Scheme and MacScheme delete the file and create a new one with the
same name, so that the previous contents of the file are destroyed. Some implementations
of Scheme may signal an error if one tries to open an output file that already exists.
(define file-copier
(lambda (infile outfile)
(let ((p-in (open- input -file infile))
(p-out (open-output-file outfile)))
(letrec ((copier (leunbda (ch)
(if (not (eof-object? ch))
(begin
(write-char ch p-out)
(copier (read-chau: p-in)))))))
(copier (read-char p-in))
(close-input-port p-in)
(close-output-port p-out)))))
When f ile->stream is invoked, the first character is read from the input
file and made the first element of the stream. No other characters are read
until they are needed. We shall describe when reading happens in this process
after we define stream->f ile. As long as the input port is open, characters
are read from that port and the stream is built. When the object denoting
the end of the file is encountered, the input port is closed and the the-null-
stream is installed.
Our strategy is first to remove all of the newlines and returns in the stream
of characters, for these are where the original line breaks were. We insert a
space wherever we remove a newline or return, for otherwise there would be
(define f ile->streain
(lambda (filensune)
(let ((port-in (open- input -file f ileneune)))
(letrec
( (build- input-stream
(lambda ()
no space separating the last word on one line from the first word on the next
line. Insome cases, we may have put more than one space between words, so
we next eliminate all excess spaces; that is, whenever there are more spaces
than one between words, the extra ones are removed. Next we insert double
spaces at the end of each sentence. Finally, we count the characters and
insert a newline character so as to give us a line length not exceeding the
desired amount given by line-length and then write the resulting stream to
the desired output file. These operations are performed so that the output
of one is the input of the next one. The procedure formatter applies these
operations one after the other. The three arguments to formatter are a string
giving the name of the input file, a string giving the name of the output file,
and the desired line-length of the output. We first look at the definition of
formatter in Program 15.31 and then proceed to the definitions of each of
the operations that formatter composes.
The procedure that removes the newlines from the input stream is given in
Program 15.32.
If there is more than one space between words, the excess is removed by
(define formatter
(lambda (input-file output-file line-length)
(stream->f ile output-file
( insert -newlines line-length
(insert -double-spaces
(remove-extra-spaces
(remove-newlines
(f ile->stream input-file))))))))
(define remove-newlines
(leunbda (str)
(stream-map
(lambda (ch)
(case ch
((#\return #\newline) #\space)
(else ch)))
str)))
(define remove-eztra-spaces
(lambda (str)
(cond
((stream-null? str) str)
((char=? (stresun-car str) #\space)
(stream-cons #\space
(remove-extra-spaces
(trim-spaces (stream-cdr str)))))
(else (stream-cons
(stream-car str)
(remove-extra-spaces (stream-cdr str)))))))
(define trim-spaces
(lambda (str)
(cond
((stream-null? str) str)
((char=? (stream-car str) #\space)
(trim-spaces (stream-cdr str)))
(else str))))
(define end-of-sentence?
(leunbda (ch)
(or (char=? ch «\.) (char=? ch «\ ! ) (char=? ch #\?))))
gram 15.34, to remove any that might be left. Although at most one space
needs to be inserted because we know that for this problem, there is exactly
one space following a sentence terminator, it is better to make the procedure
do what its and be more independent of its input. The
specification dictates
helping procedure end-of-sentence? in Program 15.36 merely tests whether
its argument is a period, an exclamation mark, or a question mark.
(define count-chars-to-next-space
(lambda (strm)
(letrec
( (count-ahead
(lambda (str count)
(cond
((stream-null? str) count)
((char=? (stream-car str) #\space) coiint)
(else (count-ahead (stream-cdr str) (addl count)))))))
(count-ahead stm 0))))
The last step before writing the reformatted stream to the output file is
the stream-car in stream->f ile starts the process. That demand is prop-
agated through insert-newlines, insert-double-spaces, remove-extra-
spaces, remove-newlines all theway to f ile->stream. Then the procedure
f ile->stream responds to the demand by actually reading the next charac-
ter.That character is sent back to remove-newlines, which decides if it has
enough information to send the next character to remove-extra-spaces, etc.
Thus the demand is propagated down the procedures, and values are propa-
gated up through the procedures. We see that only the minimal information
is used at any one time.
This extended example illustrates the use of input and output to files and
the use of streams in reading from and writing to a file and processing the
information stored in the stream. There are many ways in which this formatter
can be modified to take into account textual features that it now ignores. For
example, the quotation mark that ends a sentence is usually after the period,
and this program will insert spaces after the period. It also does not preserve
blank lines that separate paragraphs or paragraph indentation. It makes an
interesting exercise to add some of these features to the formatter.
Exercise 15.24
A file contains a column of integers, one per line. Write a procedure that reads
the integers in this file and produces another file that contains two columns:
the first column containing the integers in the original file and the second
containing the running sum of the integers in the first column. The number
of integers in the original file is not specified. Place an appropriate header at
the top of each column.
Exercise 15.25
A file contains a column of integers, one per line. Write a procedure that reads
the integers in this file and produces two additional files that contain two
columns: the first column of both files contains the integers in the original file
and the second contains the running sum and running product, respectively,
of the integers in the first column. The number of integers in the original file
Exercise 15.26
Write a program that will count the number of words in a file containing text.
You may make reasonable eissumptions about the nature of the text.
The next six problems are related. Work them in order and you will discover
Exercise 15.27
Test formatter developed in this section.
Exercise 15.28
Test the procedure formatter defined below. In order to do this, you will
(define fomatter
(lanbda (input-file output-file line-length)
((8treaim->file output-file)
((insert-newlines line-length)
(insert-double-spaces
(remove-extra-spaces
(renove-newlines
(file->8tream input-file))))))))
Exercise 15.29
Test the procedure formatter defined below. In order to do this, you will
need to pass output-file and line-length first and then pass input-file
to that result.
(define formatter
(leunbda (output-file line-length)
(lambda (input -file)
( (stream->f ile output-file)
(define apply-procedures
(lambda (procedures)
(if (null? procedvires)
(lambda (x) x)
(compose
(car procedures)
(apply-procedures (cdr procedures))))))
Exercise 15.31
Test the procedure formatter defined below, which uses compose from the
previous exercise.
(define formatter
(lambda (output-file line-length)
(compose
(stream->f ile output-file)
( insert -newlines line-length)
insert-double-spaces
remove-extra-spaces
remove-newlines
f ile->stream) )
b. (compose
(stream->f ile output-file)
remove-newlines
f ile->stre2uii)
C. (compose
(stream->f ile output-file)
remove-extra-spaces
remove-newlines
f ile->stream)
d. (compose
(stream->f ile output-file)
(insert-newlines line-length)
remove-ertra-spaces
remove-newlines
f ile->stream)
e. (compose
(stream->f ile output-file)
(insert-newlines line-length)
remove-newlines
f ile->stream)
Control
cafe in Paris whose name we have long since forgotten, there is one way we
can relive the experience. We may ask the genie to rub a magic liquid on a
photograph. When that happens, we escape to the same cafe where we were
long ago. We will have the same waiter and perhaps order the same food.
Whether the waiter aged or not, or whether we are heavier, will depend on
whether changes have occurred. If not, then we are the same. If so, then some
aspects may be the same, like the cafe, but other aspects may have changed.
Perhaps the genie rubbed the wrong photograph, and instead of rubbing the
photograph to get us to the cafe, he rubbed the photograph of us paying the
waiter. What a shame, thrust back to that delicious cafe and not reliving the
meal. What happens after the meal is over? You have two choices. You can
stay in Paris and enjoy the night life, as you did long ago, or you can ask the
genie to rub another photograph. Each time one is rubbed, you are escaping
to another point in your past but with possible changes.
514 Control
—
16 Introduction to Continuations
16.1 Overview
Did you ever lie in bed early in the morning and think about what you were
going to do that day? Your thinking probably led to something like this: "I've
got to shower, then brush my teeth, eat breakfast, find my way to campus, and
get to my first class. After I get to my first class, I'll think about what I have
left to do for the rest of the day." You packaged the rest of the day into a single
concept, relative to some point in the morning. You did not consciously figure
out what you would do with the rest of the day; you formed an abstraction
of the rest of the day. This notion can carry over to computations as well.
cape procedures — must be acquired. The first concept formalizes the creation
of a procedure with respect to a subexpression of an expression. The second
characterizes a procedure that upon invocation does not return to the point
of its invocation. A continuation is a context that has been made into an
escape procedure. Such continuations are created by invocations of call-
with-current-continuation.
We have already encountered an escape procedure, error. When error gets
invoked, its context, a procedure that represents the rest of the computation,
is abandoned. Consider the very simple expression:
16.2 Contexts
(+ 3 (* 4 (+ 5 6)))
The expression is evaluated using the following scheme. First, add 5 and 6
and get 11. Next multiply 11 by 4, yielding 44, and then increase that result
by 3. Now, what is the context of (+ 5 6) in that expression? We must find
a procedure that, if passed the value 11, will produce 47. There are lots of
such procedures, but we will find one by using a simple two-step technique.
In the first step we replace e, that is, (+ 5 6), by D. In the second step, we
form a procedure from the value of the result of the first step wrapped within
(lambda (D) . . . ). The context of (+ 5 6) in
(+ 3 (* 4 (+ 5 6)))
(lambda (D)
(+ 3 (* 4 D)))
( (+ (* 3 4) 5) 2)
(lambda (D)
(* (+ D 5) 2))
first step was replace a subexpression by D. Now we extend the first step
by evaluating the expression with the hole. When evaluation can no longer
proceed because of the hole, we have finished the Thus contexts are first step.
(if (zero? 5)
(+ 3 (* 4 (+ 5 6)))
( (+ (* 3 4) 5) 2))
In finding the context of (* 3 4), the result of the first step is what is left
after evaluating
(if (zero? 5)
(+ 3 ( 4 (+ 5 6)))
(* (+ D 5) 2))
(lambda (D)
(* (+ D 5) 2))
(let ((n D)
(if (zero? n)
(writeln (+ 3 (* 4 (+ 5 6))))
(writeln (* (+ (* 3 4) 5) 2)))
n)
(begin
(writeln ( (+ D 5) 2))
n)
(lambda (D)
(begin
(writeln (* (+ D 5) 2))
n))
(define tester
(lambda (n)
(if (zero? n)
(writeln (+ 3 (* 4 (+ 5 6) ))
(writeln (* (+ ( 3 4) 5) 2)))
n))
10 (tester D), then the context would be formed from the value of:
(lambda (D)
( 10 (begin
(writeln (* (+ D 5) 2))
n)))
(begin
(writeln 0)
(let ((n D)
(if (zero? n)
(writeln (+ 3 (* 4 (+ 5 6))))
(writeln ( (+ (* 3 4) 5) 2)))
n))
(begin
(writeln 0)
(let ((n D)
(if (zero? n)
(writeln (+ 3 (* 4 (+ 5 6))))
(writeln (* (+ D 5) 2)))
n))
(lambda (D)
(begin
(writeln (* (+ D 5) 2))
n))
A context might involve the use of set ! . The example below is similar to
the last one, except that within the scope of the let expression is an assignment
to the local variable n. The context of (* 3 4) in
(begin
(writeln 0)
(let ((n D)
(if (zero? n)
(writeln (+ 3 (* 4 (+ 5 6) ) )
(writeln (* (+ (* 3 4) 5) 2)))
(set ! n (+ n 2))
n))
is the value of
(lambda (D)
(begin
(writeln (* (+ D 5) 2))
(set ! n (+ n 2))
n))
The free variable n, initially 1, is taken from the let expression. Each time
the context is invoked, the variable n is incremented to the next positive odd
integer, and what gets subsequently returned is also increased. If <c> is this
context, then the first invocation of <c> assigns 3 to n, and the second invo-
cation assigns 5 to n. From the way in which n changes upon each invocation
of <c>, it follows that contexts are procedures that may even maintain state.
In the next example, we look at the terminating condition of a recursive
(lambda (D)
(cons (cons 2 (cons 4 (cons 6 (cons (+ 3 D) '()))))))
stops the computation. Consider the simple procedure sum+n, which adds n
to the sum of the numbers from 1 to n:
(define siim+n
(lambda (n)
(if (zero? n)
(* 10 (sum+n 5)) =»
(* 10 (if (zero? 5) (+ (addl 5) (siiin+n (subl 5))))) =^
(* 10 (+ 6 (sum+n 4))) ^*
(* 10 (+ 6 (if (zero? 4) (+ (addl 4) (sum+n (subl 4)))))) =>
(* 10 (+ 6 (+ 5 (sum+n 3)))) =>
(* 10 (+ 6 (+ 5 (if (zero 3) (+ D (sum+n (subl 3))))))) =*
(* 10 (+ 6 (+ 5 (+ D (sum+n 2)))))
(lambda (D)
(* 10 (+ 6 (+ 5 (+ D (sum+n 2))))))
(lambda (D)
(if (zero? D) 8 9)).
When this context is applied, its value will be 8 or 9, depending on what value
gets bound to D.
In order to understand continuations, you will need to have lots of experi-
ence forming contexts. The exercises below should give you enough practice.
Exercises
Exercise 16.1
What is the context of (cons 3 '())in(cons 1 (cons 2 (cons 3 '())))?
What results when we apply this context to '(a b c), '(x y), and '
(3)?
^ The trace that follows assumes a left to right order of evaluation of the operands to +.
The procedure map-addl imposed a left to right order of evaluation of the operands to cons
by using a let expression.
part, describe the context of each expression; in the second part, determine
the resultant values found by sequentially applying the context to each of 5,
6, and 7.
a. (+ a b) in ( c (+ a b)).
b. x in (+ X y).
c. y in (- X y).
e. (* c (+ a b)) in (+ d (* c (+ a b))).
g. X in (if X y z).
Exercise 16.3
For each expression below, determine the context of (cons 3 ' (4)) and the
result of applying that context to (1 2 3).
(* n (f (subl n)))))))
(f 3))
and
(+ ((escaper *) 5 2) 3)
evaluates to 10.
Consider the invocation:
(+ ((escaper
(lambda (x)
(- (* I 3) 7)))
5)
4)
((lambda (x)
(- (* X 3) 7))
5)
(+ ((escaper
(lambda (x)
((escaper -) (* x 3) 7)))
5)
4)
2. (+ ((escaper
(lambda (x)
(- ((escaper *) x 3)
7)))
5)
4)
3. (+ ((lambda (x)
((escaper -) ((escaper *) x 3)
7))
5)
17)
4. (+ ((lambda (x)
(- ((esraper ) x 3)
7))
5)
2000)
Does this fully characterize the behavior of escape procedures? Not quite.
Consider the following:
(/ (+ ((escaper
(lambda (x)
(- ( X 3) 7)))
5)
4)
2)
The awaiting addition is abandoned. Is the division, which awaits the addi-
tion, also abandoned? Yes. Since the division awaits the addition and since
the addition hcis been abandoned by the escape invocation, the division has
Exercises
Exercise 16.
Evaluate each of the following:
Exercise 16.5
Using the definition of es-cons from the previous exercise, determine the con-
text of (es-cons 3 '()) in (es-cons 1 (es-cons 2 (es-cons 3 '()))).
(define reset
(leuDbda ()
( (escaper
(lambda ()
(writeln "reset invoked"))))))
Exercise 16.7
Let e be an escape procedure, and let / and g be any procedures. To what
is (compose g (compose / e)) equivalent? Can this be generalized to an
arbitrary number of procedure compositions?
Exercise 16.8
Let / be any procedure. When can / be replaced by (escaper /) and still
(+ 3 ( 4 (call/cc r)))
That is, after the system forms the context of (call/cc r) the system passes ,
(+ 3 (* 4 ((lambda (continuation) 6)
(escaper (lambda (D) (+ 3 ( 4 D)))))))
((lambda (continuation) 6)
(escaper (lambda (D) (+ 3 ( 4 D)))))
and then the result is 27. Remember, an escape invocation abandons its
context, so (lambda (D) (+3 (*4 (+2 n))))is abandoned, contin-
uation has the value (escaper (lambda (D) ... D ...)). Because the
context of a call/cc invocation is turned into an escape procedure, we use
the notation <ep> for procedures that get passed to r.
Exercise 16.9
For each expression below, there are four parts. In Part a, determine the
expression's value. In Part b, define r locally using let, and form the original
application of (call/cc r), which leads to this expression. In Part c, define
r globally, and in Part d, using the global r, form the original application of
(call/cc r), which leads to this expression. The solution to problem [1] is
given below:
a. -22
c. (define r
(lambda (continuation)
(continuation 5)))
d. (- 3 (* 6 (call/cc r)))
Exercise 16.10
If r is
We next consider three simple experiments. Each experiment includes one use
of a receiver (remember that a receiver is just a single-parameter procedure)
without using call/cc and one that uses call/cc. The point of these experi-
ments is to show the simple behavioral characteristics of call/cc expressions.
Although the differences may seem minor in the first two experiments, their
differences are important. In the last experiment, however, the differences
demonstrate the unusual behavior of continuations. The receivers we use to
demonstrate these properties are presented in Program 16.2.
(define receiver-1
(lambda (proc)
(proc (list 1))))
(define receiver-2
(lambda (proc)
(proc (list (proc (list 2))))))
(define receiver-3
(lambda (proc)
(proc (list (proc (list 3 proc))))))
(define writeln/retum
(lambda (x)
(writeln x)
x))
(define einswer-maker
(Icuubda (x)
(cons 'answer-is (writeln/return x))))
(define call
(Izunbda (receiver)
(receiver writeln/return)))
the value of
(leunbda (proc)
(proc (list 1)))
A.
B.
(escaper
(lambda (D)
(set! resultcc (answer-meiker D))))
Then this continuation is invoked on (list 1), and since it is an escape pro-
cedure, that is all that happens. The procedure einswer-meiker is invoked on
(list 1), causing (1) to appear, and its result, (eoiswer-is 1), is assigned
to resultcc. At [4] we verify that resultcc is indeed (einsHer-is 1).
For Experiment 2, recall that receiver-2 is the value of:
(lambda (proc)
(proc (list (proc (list 2)))))
Experiment 2:
A.
B.
In Part A the main difference is the extra set of parentheses around the value,
which is the result passed to answer-maker. Both invocations of proc do
a writeln/return. The first time is with (2) as its argument. When this
returns, its argument is passed to list, resulting in ( (2) ). Now we are ready
for the second invocation of writeln/return. It displays argument ((2))
its
(lambda (D)
(set! resultcc (answer-maker (proc (list D)))))
The list invocation and the proc invocation waiting for the result of list are
abandoned. The list invocation not occurring accounts for the missing set
of parentheses, and the proc invocation not occurring accounts for why only
one (2) is displayed. In Part B when proc, the continuation, is invoked, its
argument is passed to the waiting answer-msJcer. The value (2) is displayed,
and the result (answer-is 2) is sent to the waiting set!. The set! causes
the value (answer-is 2) to be associated with resultcc. The result of the
experiment is verified at [4]
We have come to our last experiment. This one is slightly trickier than the
earlier ones. Because of we discuss all of Part
this, A before we look at Part
B. We recall that receiver-3 is the value of
(lambda (proc)
(proc (list (proc (list 3 proc)))))
Experiment 3:^
' To denote the procedure that is the value of the veu'iable procedure-name, we use the
notation <procedure-naTiie>
(.<writeln/return>
(list i<writ€ln/return>
(list 3 <writeln/return>))))
B.
(escaper
(lambda (D)
(set! resultcc (answer-maker D))))
but since <ep> is invoked, the outer list, <ep>, and answer-maker invo-
cations are abandoned, as well as the set! expression. Therefore, the result
of (call/cc receiver-3) is the result of invoking (<ep> (list 3 <ep>)).
The escape procedure <ep> is invoked giving the value (3 <ep>) as the value
that is passed to eoiswer-maker, which displays the list (3 <ep>). Next
answer-is is consed to the front of (3 <ep>), which yields (answer-is 3
<ep>). Then is done, which changes the value of resultcc.
the set! At
[6] , we what was
verify that expected has indeed occurred. We are about to
execute the code at [7]. The expression (3rd resultcc) yields the escape
procedure <ep> that was saved earlier. It is passed the list (1000). What is
(.<ep> (list 1000))? Recall that <ep> is an escape procedure that passes
its argument to answer-maker and then assigns to resultcc the result of
the value of resultcc. This time it has been changed to (answer-is 1000)!
Although the set ! was done back at [5] , the escape procedure <ep> included
doing everything again once it was invoked.
Exercises
Exercise 16.12
Rewrite aoiswer-maier using call.
Exercise 16.13
Run the experiment with exer-receiver.
(define exer-receiver
(lambda (proc)
(list (proc (list 'exer proc)))))
Exercise 16.1
For each expression below, describe the binding that continuation gets, and
give the value(s) of the expression. Each expression must be tested more than
once. We include the solution for Part a.
Exercise 16.15
Determine the outcome of Experiment 3 with [1] and [5] replaced by the
expressions below.
[1] (begin
(set! result (cuiswer-meJcer (call receiver-3)))
'done)
Exercise 16.16
We define a procedure map-subl that takes a list of numbers and returns a
list with each element of the list decremented by one. In addition to doing
the work of map-subl, it also sets the global variable deep to a continuation.
(define map-subl
(leunbda (Is)
(if (null? Is)
(let ((receiver (lambda (k)
(set! deep k)
'())))
(call/cc receiver))
(cons (subl (ccir Is)) (map-subl (cdr Is))))))
(escaper
(lambda (Q)
(cons 1000 (cons -1 D))))
(escaper
(lambda (Q)
(cons 1000 (cons (cons -1 D)))))
the experiment.
(define escaper
(leUibda (proc)
(lambda (x)
(escape/thunk* (lambda () (proc x))))))
(define receiver-4
(Icunbda (continuation)
(set! *escape/thunk* continuation)
(*escape/thimk* (launbda () (writeln "escaper is defined")))))
We then have:
Finally,
(define escaper
(lambda (proc)
(lambda args
(escape/thunk*
(lambda ()
This definition of escaper can be used to test all the results and exercises of
this chapter.
Exercises
Exercise 16.17
Assume the existence of escaper and then define *escape/thunk* with es-
caper. You may not use call/cc.
Exercise 16.18
Determine the value of (/ 5 (*escape/thunk* (lambda () 0))).
Exercise 16.21
Determine the value of the following expressions:
Exercise 16.22
Consider the procedure new-escaper below.
Are new-escaper and escaper the same? Why is new-escaper better than
escaper?
(define hos-many-till
(lambda (n teirget)
(let ((count 0))
(cycle-proc
(lambda ()
(define hos-many-till
(lambda (n target thresh)
(let ((receiver
(lambda (exit-above-threshold)
(let ((count 0) (svim 0))
(cycle-proc
(lambda ()
(define random-data
(lambda (n thresh)
(letrec ((loop (leunbda (target;
(cond
((negative? target) '())
(else (cons (how-m£uiy-till n teurget thresh)
(loop (subl target))))))))
1
(escaper
(lambda (Q)
(cons D (loop (subl target)))))
Exercise
Exercise 16.23
Explain why the test for termination within rsuidoni-datais (negative? tar-
get).
The call/cc operator gives the ability to escape from recursive computations
while basically throwing out all the work that has stacked up. A simple exam-
ple clarifies in what sense the mechanism avoids doing pending computations.
We look at the problem of taking the product of a list of numbers and adding
(define product+
(lanbda (n nums)
(letrec
((product (laabda (nu»s)
(cond
((null? nuMs) 1)
(else ( [* icdir nuns) (product (cdr nu»8))))))))
(let ((prod (product nuns)))
(if (zero? prod) (+ n prod))))))
zero. This version is in Program 16.12. Consider the following subtle fact:
Finding a zero in the list does not stop the computation of product. In fact,
what happens is that if the first zero is in the kth position, then there are
k — 1 multiplications using zero. This is because the context of the product
(define product*
(lambda (n nvuns)
(letrec
((product (lambda (nums)
(cond
((null? nums) 1)
((zero? (car nvuns)) 0)
(else (* (car nums) (product (cdr nums))))))))
(let ((prod (product nums)))
(if (zero? prod) (+ n prod))))))
(escaper
(lambda (D)
(+ 100 D)))
is invoked, and the result is 100. This follows because the continuation is
We see that finding a zero in the list produces a value to pass to the contin-
uation formed from the invocation of (call/cc receiver) and finishes the
computation of product*. Moreover, we observe the rather surprising fact
that if there is a zero in the list, then no multiplications occur regardless of
where in the list that zero occurs.
(define product*
(lanbda (n nuns)
(let ((receiver
(lambda (exit-on-zero)
(letrec
((product (lambda (nums)
(cond
((null? nums) 1)
((zero? (ceir nums)) (exit-on-zero 0))
(else (* (car nums)
(product (cdr nums))))))))
(let ((prod (product nums)))
(if (zero? prod) (+ n prod)))))))
(call/cc receiver))))
(define product*
(lambda (n nums)
(let ((receiver
(lambda (exit-on- zero)
(letrec
((product (lambda (nums)
(cond
( (null? niims) 1)
((zero? (car lums)) (exit-on-zero 0))
]
(define product*
(lanbda (n nims)
(let ((receiver
(la«bda (exit-on-zero)
(letrec
((product
(leunbda (nuns)
(cond
((null? nuBs) 1)
((number? (car nums))
(cond
((zero? (car nuns)) (exit-on-zero 0))
(else (* (cen: nuns)
(product (cdr nu»s))))))
(else (* (product (car nums))
(product (cdr nums))))))))
(+ n (product nums))))))
(call/cc receiver))))
(define *-and-count-m2Jcer
(lambda
(let ((local-counter 0))
(lambda (nl n2)
(set! local-counter (+ local-counter 1))
(writeln "Number of multiplications « " local-counter)
(* nl n2)))))
which results in 0. However, if the had been a 3, then the result would have
been 77. The new definition of product + is given in Program 16.15.
Some, but not all, multiplications are avoidable. By counting the number of
multiplications, we can discover how many can be avoided. This can be done
by invoking a special multiplication procedure *-and-count-maJter, given in
Program 16.16, and then passing the result of its invocation as an argument
to product+. The procedure product+ in a functional style would have once
again introduced all those multiplications by zero. (See Program 16.17.) Thus
we can invoke:
(define product+
(leunbda (n nums *-proc)
(letrec
( (product
(lambda (nums)
(cond
((null? nums) 1)
When product+ of Program 16.17 is used on the given tree, there are 12
multiplications, and when product+ of Program 16.15 is used there are fewer
than 12 multiplications. There is, of course, a way to avoid all multiplications,
but it involves walking through the entire list looking for O's before starting the
multiplication process. This makes the algorithm two-pass (it would require
two pcisses through the list).
Exercises
Exercise 16.24
Run product + of Program 16.14 with the *-proc argument over a list of
numbers to verify the claim that no multiplications occur if the list contains a
0. Run product + of Program 16.12 with the *-proc argument over the same
list to compare with the first part of this exercise.
argument over the same list to compare with the first part of this exercise.
Exercise 16.26
Rewrite product + of Program 16.15 where n is always 0.
Exercise 16.27
Rewrite product+ of Programs 16.14 and 16.15 using a local variable to main-
tain the accumulating product. Can this be done without using call/cc?
17.1 Overview
then ignored, the following hold, where the use of ellipses surrounding an
expression indicates that the expression may be embedded:
. )
. . . body . .
and
= (receiver 'anything)
= body
where the next to the last equality holds since receiver is an escape proce-
dure, and the last equality holds since continuation is ignored.
and
(define countdown
(launbda (n)
(writeln "This only appears once")
(let ((pair (message "Exit" (attempt (message "Enter" n)))))
(let ((v (1st pair))
(returner (2nd pair)))
(writeln " The non-negative-number: " v)
(if (positive? v)
(returner (list (subl v) returner))
(writeln "Blastoff"))))))
The value of proc is just the identity procedure we denote as <proc>. Here
is what appears when (countdown 3) is invoked:
(define message
(lambda (direction value)
(writeln " " direction "ing attempt with value: " value)
value))
(define attempt
(lambda (n)
(let ((receiver (lambda (proc) (list n proc ))))
(receiver (lambda (x) x)))))
"This only appeairs once" appeeirs once. The next event is an attempt
to find the value of the expression (message "Enter" 3). This produces
the message, "Entering attempt vith value: 3" and message returns its
(define attempt
(lambda (n)
(let ((receiver (lambda (proc) (list n proc))))
(call/cc receiver))))
"This only appears once" appears once. The next event is an attempt
to find the value of the expression (message "Enter" 3). This produces
the message, "Entering attempt with value: 3" and message returns its
Clearly we are in a loop, with v replaced by (subl v). The loop terminates
when V is no longer positive. An important point is that call/cc is invoked
only once. Therefore, we know for certain that <ep> is always the same
continuation. The procedure attempt of Program 17.4 is invoked only once
and its body is never reentered. This follows because the sentence "Entering
attempt with value: n" appears only when n is 3.
Exercise
In this section we consider an experiment where we use more than one con-
tinuation. Everything until now has worked with just one continuation. Now
we shall use several continuations. To keep track of the full meaning of each
continuation, we shall plug in values for variables that will not change. This
frees us from having to remember their values for use later.
(define receiver
(lambda (continuation)
(continuation continuation)))
(define test(ar
(lambda (continuation)
(writeln "beginning")
(call/cc continuation)
(writeln "middle")
(call/cc continuation)
(writeln "end")))
Experiment:
The first event is to form <ep>, which, if it ever gets an argument, passes
(escaper
(lambda (D)
(tester D)))
(escaper
(laabda (D)
D
(writeln "iddle")
(call/cc <cp>)
(writeln "end")))
The continuation <epa> ignores its argument, D, displays middle, then in-
vokes (call/cc <ep>), and when that returns, it displays end. Now recall
that <ep> takes its argument and invokes (escaper tester) on its argument,
so continuation is bound to <epa>. We write beginning. We next invoke
(call/cc <epa>). This causes us to create <epb>. Before we figure out any-
thing about what <epa> does with <epb>, we must understand what <epb>
does if it ever gets invoked. <epb> is the value of:
(escaper
(laabda (D)
D
(writeln "Middle")
(call/cc <epa>)
(writeln "end")))
The continuation <epb> ignores its argument, displays middle, then invokes
(call/cc <epa>), and when that returns, it displays end. Now recall that
<epa> takes its argument (ignores it) and displays middle, which we do now,
and then invokes (call/cc <ep>). Once agaun we form the new continuation
<epc>, which is the value of:
(escaper
(lambda (D)
D
(writeln "end")))
(escaper
(lambda (D)
D
(writeln "middle")
(call/cc <epc>)
(writeln "end")))
This continuation displays middle, invokes (call/cc <epc>), and when that
returns, it displays end. What is {<epc> <epd>)? The continuation <epc> is
an escape procedure that ignores its argument and displays end. So we ignore
<epd>, after having gone to all the trouble of constructing it, and display end.
Exercises
Exercise 17.2
During the experiment, how many more continuations were formed than were
invoked?
Exercise 17.3
Determine what this expression represents:
(define f latten-number-list
(lambda (s)
(cond
((null? s) '())
((number? s) (list (break s)))
(else
(let ((flatcar
(f latten-number--list (car s))))
( append flatcar
(f latten-niiaber-list (c dr s))))))))
1
(define break
(lambda (x)
i))
(define break
(lambda (x)
(let ( (breeik-receiver
(lambda (continuation)
(continuation i))))
(call/cc break-receiver))))
that the data for the example are the same as those of prodTict+: a deep list
Hence:
Another way to write break, which uses continuations but has the same
meaning, is given in Program 17.9. This follows because we return as a value
the argument to break. Since that value is x, we get the equivalent of (lambda
(define breeJc
(lanbda (x)
(let ( (bresJt-receiver
(leuDbda (continuation)
(set! get-back (lambda () (continuation x)))
(amy-action x))))
(call/cc brezJt-receiver))))
(define any-action
(lambda (x)
(writeln x)
(get-back)))
(define any-action
(lambda (x)
((escaper (lambda () x)))
(get-back)))
(x) x). But now we have access to continuation, and, moreover, we can
characterize its behavior. Whenever break is invoked, we can think about the
(define break
(lambda (z)
(let ((break-receiver
(lambda (continuation)
(set! get-back continuation)
(any-action x))))
(call/cc break-receiver))))
(get-back), the answer is no. Is there a way to get back into the original
computation? The answer is yes. Since get-back is bound globally, we can
invoke it at the prompt. Below is an experiment using these tools.
[2] (get-back)
2
[3] (get-back)
3
[4] (get-back)
(1 2 3)
The procedure break has a limitation. There is no control over what value is
[2] (get-back 4)
2
"
(define breeJc-argunent any value")
(define smy-action
(lambda (x)
(set ! bresJt-argxment x)
((escaper (lambda () x)))))
[3] (get-back 5)
3
[4] (get -back 6)
(4 5 6)
In this case, if x is locally bound at the time of invocation of break, the list
composed of these two procedures gives a lot of power to affect the internal
state of a computation. Program 17.16 shows how flatten-number-list
changes to support break. Whenever breaJc occurs, breaOt-ao-giunent gets
bound to a two-element list and we define the extract and store procedures
as shown in Programs 17.17 and 17.18.
This is just the tip of an iceberg. We are concerned only about one vari-
able. This idea for debugging can be generalized to lists of arbitrarily many
variables, but its utility diminishes as the number of variables increases. If
(define break
(lambda (z)
(let ((break-receiver
(lambda (continuation)
(set! get-back continuation)
(set ! break-argument x)
((escaper (lambda () x))))))
(call/cc break-receiver))))
(define extract
(lambda ()
there are too many variables, it may be time to redesign the procedures. With
breatk we have seen how there are many continuations coming from one pro-
(define store
(laabda (value)
((2nd break-eurguaent) value)))
eventually invoked after escaping to the prompt. The escape to the prompt is
not very exciting. In the next section we allow far more interesting behavior to
balance each continuation. Because the behavior of such uses of continuations
is balanced, these continuations are called coroutines.
Exercises
(define f latten-nuaber-list
(laBbda (s)
(letrec
((flatten
(laabda (s)
(cond
((null? s) '())
((nuBber? a) (break (list s)))
(else (let ((flatcar (flatten (car s))))
(append flatcar (flatten (cdr s) ))))))))
(flatten s))))
Exercise 17.5
Consider how we can repeat the results of the first experiment using flatten-
number-list of Program 17.16. A condition imposed on this exercise is that
no number may be input from [2] to the end of the experiment. Hint: Do
not use store.
(define product+
(lambda (n Is)
(letrec ((product
(lambda (Is)
(cond
((null? Is) 1)
Experiment with
(break-var var)
Exercise 17.8
Consider the following experiment:
[2] (get-back 4)
2
[3] (f latten-number-list '((5 6 7) 8))
5
Exercise 17.9
Consider the results of the experiment from the previous exercise. How would
the results differ if the list of continuations were treated like a queue instead
of a stack?
The board game's control flow is very regular. A plays, then B plays, then
C plays, then A plays, and so on. As a result, not enough of the generality of
coroutines can be seen through a board game simulation. If each player deter-
mined randomly which opposing player was to play next, this would require
much of the generality of coroutines. Rather than using random numbers
we simply picked an unnatural ordering that is illustrated in Program 17.19.
Remember that nothing is displayed in a writeln expression until all of its
operands have a value. Now if we invoke (A '*) we get the following output:
[1] (A '*)
This is A
This is B
This is C
Came from C
Back in A
Came from A
Back in C
Came from C
Back in B
Came from B
Let us see what it takes to make these programs work. We need the pro-
cedure coroutine-maker, which takes a procedure as an argument. This
argument is a procedure that obtains a meaning for resume and v when it is
(define A
(let ((A-proc (lambda (resume v)
(writeln "This is A")
(writeln "Came from " (resume B "A"))
(writeln "Back in A")
(writeln "Came from " (resume C "A")))))
(coroutine-mciker A-proc)))
(define B
(let ((B-proc (lambda (resume v)
(writeln (blanks 14) "This is B")
(writeln (blanks 14)
"Came from " (resume C "B"))
(writeln (blanks 14) "Back in B")
(writeln (bleinks 14)
"Came from " (resume A "B")))))
(coroutine-meiker B-proc)))
(define C
(let ((C-proc (leimbda (resume v)
(writeln (blanks 28) "This is C")
(writeln (blanks 28)
"Came from " (resume A "C"))
(writeln (blanks 28) "Back in C")
(writeln (blanks 28)
"Came from " (resume B "C")))))
(coroutine-maJcer C-proc)))
structure of resumer is
(define coroutine-maker
(leuabda (proc)
(let ((saved-continuation "any continuation"))
(let ((update-continuation!
(lambda (v)
(set! saved-continuation v))))
(let ((resumer (resume -msJcer update-continuation!))
(first-time ft))
(leimbda (value)
(if first-time
(begin
(set! first-time if)
(proc resumer value))
(saved-continuation value) )))))))
(define resume-mciker
(lambda ( update -proc !
Thus far the code has not shown us where the continuations are being cre-
ated. In coroutine-maker, this is done in the procedure formed by invoking
(resume-maker update-continuation ! )
When resumer is invoked with a coroutine, say B, and a value, say "V",
a continuation bound to continuation. That continuation is stored in
is
Exercises
Exercise 17.10
To clarify the behavior of coroutine-msQcer and resume-meJcer, we used many
variables. Very few are required. Furthermore, resume-maicer itself is not nec-
essary. Using this knowledge, rewrite coroutine-meiker with as few variables
as possible.
Exercise 17.11
Look at the results of the previous exercise. If there is a first-time flag,
Exercise 17.12
Study the definitions of ping and pong below:
(define ping
(let ((ping-proc (lambda (resume v)
(display "ping-")
(resume pong 'ignored-ping))))
(coroutine-maker ping-proc)))
(define pong
(let ((pong-proc (leunbda (resume v)
(display "pong")
(newline)
(resume ping 'ignored-pong))))
(coroutine-mciker pong-proc)))
Now we are ready to look at Grune's problem (Grune 1977). The problem is
described as follows:
Input ^ k ^ B ^ Output
we can think of the flow of requests emanating at Output. Requests for values
flow from right to left and values, themselves, flow from left to right. This is
symbol. This symbol is also sent to A. Now A has enough information to send
something to B. Here are the conditions under which information flows to the
right. In these rules, x and y are the symbols in question, q is not the same
cis X, where x is a (respectively, b) and y is 6 (respectively, c):
(define reader
(lajBbda (right)
(let ((co-proc (lambda (resume v)
(cycle-proc
(lambda
(resume right (prompt-read "in> ")))))))
(coroutine-meiker co-proc))))
The action of Input sends to its right neighbor whatever it read after first
displaying a prompt:
Given that these are the basic actions, it is a relatively simple task to make
sure all free variables have the correct values and that each code segment is
work:
(define writer
(lEunbda (left escape-on-end)
(let ((co-proc (lambda (resume v)
(cycle-proc
(lambda ()
(define x ->y
(lambda (x y left right)
(let ((co-proc (lambda (resume v )
(cycle-proc
(lambda ()
(letrec
((Input (reader A))
(A (x->y 'a 'b Input B))
(B (x->y 'b 'c A Output))
(Output (writer B escape-grune)))
(Output 'ok))
(define grune
(laabda
(let ((grune-receiver
(laabda (escape-grune)
(letrec
((Input (reader (lambda (v) (A v))))
(A (x->y 'a 'b (lambda (v) (Input v)) (launbda (v) (B v))))
(B (x->y 'b 'c (lambda (v) (A v)) (lambda (v) (Output v))))
(Output (writer (lambda L (v) (B v)) escape--grune)))
(Output 'ok)))))
(call/cc gnme-receiver) ) )
writer have been carefully designed to avoid invoking any of their corou-
tine arguments: Input, A, B, and Output. Here is the problem. All of the
procedures are being created at the same time as they are being passed as
arguments. For example, to create Input, we need A, and to create A, we need
Input. To solve this problem, we must freeze the coroutines that are argu-
ments in the right-hand sides of definitions. This has the effect of postponing
the evaluation of the variables that refer to the coroutines. Unfortunately, if
we freeze these variables, we get the wrong arity; that is, coroutines take one
argument, but frozen objects (i.e., thunks) take no arguments. The code that
follows, however, works:
(letrec
((Input (reader (lambda (v) (A v))))
(A (x->y 'a 'b (lambda (v) (Input v)) (lambda (v) (B v))))
(B (x->y 'b 'c (lambda (v) (A v)) (lambda (v) (Output v))))
(Output (writer (lauibda (v) (B v)) escape-grune)))
(Output 'ok))
Program 17.25 shows the final definition of grune with all the necessary
uses of (lambda (v) ( v)). In the exercises, we develop a more natural
way to think about this unusual behavior.
This works in all cases but one: when args is a free variable in the proc
expression. Rewrite wrap using thunks to avoid this potential free variable
capture.
Exercise 17.14
Using the results of the previous exercise, write the syntax table entry for
wrap when proc is known to refer to a procedure of just one argument. This
is the case for the coroutines used in grime. Is free variable capture still a
problem?
Exercise 17.15
Using the results of the previous exercise, redefine gnine using wrap.
(define gnine
(lambda ()
(define process-meJcer
(lambda (f)
(let ((saved-continuation "any continuation"))
(let ((update-continuation!
(lambda (v)
(set! saved-continuation v))))
(let ((resumer (resume-maJcer update-continuation!))
(first-time #t))
(leuabda (value)
(if first-time
(begin
(set! first-time #f)
(cycle-proc
(lambda ()
(f resumer value))))
(saved-continuation value))))))))
Exercise 17.18
Using the results of the previous exercise, explain how process-maker differs
Exercise 17.19
Redesign Towers of Hanoi using coroutine-maker so that each disk is a corou-
tine. Can process-maker be used?
Exercise 17.20
Redesign the solution of the Eight Queens problem using coroutine-maker
so that each queen is a coroutine. Can process-madcer be used?
Exercise 17.21
Implement Grune's problem using streams instead of coroutines.
Exercise 17.22
Extend grune to any number of x->y pairs. Hint: This can be accomplished
by rewriting the procedure grime leaving everything else unchanged.
Exercise 17.23
Rework the previous exercise using streams.
We have not shown you all the interesting things you can think about with
continuations, but we have tried to show you some of the ways that continua-
tions can be used. Most of the time, you should be content to solve problems
with conventional procedural techniques. Occasionally you will be tempted
to use state changing operations like those we used when we worked with
object-oriented programming. And even less frequently you will run across a
need for continuations. This is your basic bag of tricks.
PROBLEMS
Problems worthy
of attack
prove their worth
by hitting back.
Hex 1 2 3 4 5 6 7
NUL DLE SP @ P (
P
1 SOH DCl j 1 A Q a q
2 STX DC2 II
2 B R b r
3 ETX DCS # 3 C S c s
4 EOT DC4 $ 4 D T d t
5 ENQ NAK % 5 E U e u
7 BEL ETB 1
7 G w g w
8 BS CAN (
8 H X h X
9 HT EM ) 9 I Y i y
A(10) LF SUB : J Z J z
B(ll) VT ESC + )
K [ k {
C(12) FF FS )
< L \ 1
1
D(13) CR OS - = M ] m }
" ~
E(14) SO RS • > N n
F(15) SI US / 7 - DEL
A1.2 Abbreviations for Control Characters
ASCII stands for the American Standard Code for Information Interchange.
(Scheme now supports the broader International Character Code.) The num-
ber at the top of a column represents the first digit, and the number at the
left of a row represents the second digit of the hexadecimal number that is
used to represent the character in that row and column. Thus the hexadeci-
mal code for the letter G, which is in the 4*^^ column and 7*'^ row, is 47. The
hexadecimal code for the letter g is 67, and for M is 4D. To get the decimal
representation for the ASCII code, multiply the number at the top of the
(4 X 16) -(- 13 = 77. The first two columns contain the control characters that
do not print. To enter those characters from the keyboard, we hold down the
CONTROL key while pressing the key for the character located four columns
to the right of the desired control character. For example, to enter BEL, we
press <CTRL>G, and to enter ESC, we press <CTRL>[.
Abelson, Harold, and Gerald J.Sussman, with Julie Sussman. 1985. Structure and
Interpretation of Computer Programs Cambridge, MA:
, MIT Press and New York,
NY: McGraw-Hill Book Company.
Bronowski, Jacob. 1978. The Visionary Eye. Cambridge, MA: MIT Press.
NJ: Prentice-Hall.
Eisenberg, Michael. 1988. Programming in Scheme. Redwood City, CA: Scientific
Press.
Friedman, Daniel P., and Matthias FeUeisen. 1988. The Little LISPer, Third Edi-
tion, Chicago, IL: SRA, and Trade Edition, Cambridge, MA: MIT Press.
McCarthy, John. 1960. Recursive functions of symboHc expressions and their com-
putation by machine. In Communications of the ACM, 3(4):184-195.
Park, Stephen K., and Keith W. MUler. 1988. Random number generators: Good
ones are hard to find. Communications of the ACM. 31(10):1192-1201.
Rees, Jonathan, and William Clinger, editors. 1986. The revised report on the
algorithmic language Scheme. In ACM SIGPLAN Noticies 2l(l2):37-79.
Steele,Guy Lewis, Jr., and Gerald Jay Sussman. 1978. The revised report on
Scheme, a dialect of Lisp. Memo 452, MIT Artificial Intelligence Laboratory.
Sussman, Gerald Jay, and Guy Lewis Steele Jr. 1975. Scheme: an Interpreter for
Extended Lambda Calculus. Memo 349, MIT Artificial Intelligence Laboratory.
Texas Instruments. 1988. PC Scheme, User's Guide & Language Reference Manual.
Redwood City, CA: The Scientific Press.
582 References