Sections 39 and 40 gradually introduced
the idea of mutable structures. In the first of the two sections we studied
the idea of changing a local
ly defined variable through a
function. In the second one, we discussed how structures could be modified,
too.
Now we need to learn when and how to use this new power. The first
subsection concerns the question of why a program should modify a structure.
The second reviews how the existing design recipes apply when we wish to
use mutators. The third one discusses some difficult cases. The last one is
dedicated to the differences between set!
and structure mutators.
Whenever we apply a structure constructor, we create a new structure. On some occasions, this is truly what we want. Consider a function that consumes a list of personnel records and produces a list of phone book entries. The personnel records may contain information about a person's address, including the phone number, date of birth, marital status, closest relatives, and salary level. The entry for the phone book should contain the name and the phone number and nothing else. This kind of program should definitely generate a new structure from each structure in the given list.
On other occasions, though, creating a new structure doesn't correspond to our intuition. Suppose we wish to give someone a raise. The only way of accomplishing this at the moment is to create a new personnel record that contains all the old information and the new salary information. Or, suppose someone has moved and received a new phone number, and we wish to update the phone book on our PDA. Just like the program that changes a person's salary level, the program that updates the phone book would create a new phone book entry. In reality, however, we would not create a new personnel record or a new entry in the phone book. We would instead correct the existing personnel record and the existing entry in our phone book. A program should be able to perform the same kind of corrective action and, with mutators, we can indeed develop such programs.
Roughly speaking, the examples suggest two cases. First, if a structure corresponds to a physical object and the computation corresponds to a corrective action, the program may mutate the structure. Second, if a structure does not correspond to a physical object or if the computation creates a new kind of value from existing information, the program should create a new structure. These two rules are not clear-cut. We will often encounter situations where both solutions are feasible. In that case, we must consider an ease-of-programming argument. If one of the two solutions is easier to design -- often the creation of a new structure, choose it. If the decision leads to a performance bottleneck -- and only then, restructure it.
Surprisingly, programming with mutators does not require any new design
recipes -- as long as the mutated fields always contain atomic values. Our
receipes work perfectly fine. While the design of non-mutating programs
requires the combination of values, programming with mutators requires the
combination of effects. Hence the key is to add a well-formulated effect
statement to a function's contract and to make up examples that illustrate
the effects. We practiced both of these activities for set!
expressions already in section 36. In this section we
learn to adapt the design recipes and effect statements to structural
mutations. To do that, we consider a short series of examples. Each
illustrates how an applicable design recipe helps with the design of
structure-modifying or vector-modifying functions.
The first example concerns the mutation of plain structures. Suppose we are given a structure and a data definition for personnel records:
(define-struct personnel (name address salary)) ;; A personnel record (PR) is a structure: ;;(make-personnel n a s)
;; wheren
is a symbol,a
is a string, ands
is a number.
A function that consumes such a record is based on the following template:
(define (fun-for-personnel pr) ... (personnel-name pr) ... ... (personnel-address pr) ... ... (personnel-salary pr) ...)
Consider a function for increasing the salary field:
;;increase-salary : PR number -> void
;; effect: to modify the salary field ofa-pr
by addinga-raise
(define (increase-salary a-pr a-raise) ...)
The contract specifies that the function consumes a PR
and a
number. The purpose statement is an effect statement, which explains how
the argument of increase-salary
is modified.
Developing examples for increase-salary
requires the techniques of
section 36. Specifcially, we must be able to compare
the before and after state of some PR
structure:
(local ((define pr1 (make-personnel 'Bob 'Pittsburgh 70000))) (begin (increase-salary pr1 10000) (= (personnel-salary pr1) 80000)))
The result of the expression is true
if, and only if,
increase-salary
works properly for this example.
We can now use the template and the example to define the function:
;;increase-salary : PR number -> void
;; effect: to modify the salary field ofa-pr
by adding ina-raise
(define (increase-salary a-pr a-raise) (set-personnel-salary! a-pr (+ (personnel-salary a-pr) a-raise)))
As usual, the full definition uses only one of several subexpressions from the template, but the template reminds us of what information we can use: the arguments and their pieces; and what parts we can modify: the fields for which we have selectors.
Exercise 41.2.1.
Make up examples for increase-salary
and test the
function. Formulate the tests as boolean-valued
expressions.
Solution
Exercise 41.2.2.
Adapt increase-salary
such that it accepts only values for
a-raise
between 3% and 7% of the salary. It calls error
otherwise.
Solution
Exercise 41.2.3.
Develop increase-percentage
. The function consumes a PR
and a percentage between 3% and 7%. It increases the value in the salary
field of the PR
by the lesser of the percentage increase or
7000
.
Solution
Exercise 41.2.4.
Develop the function new-date
. It consumes a cheerleader
record and adds a date to the beginning of a list. Here are the relevant
definitions:
(define-struct cheerleader (name dates)) ;; A cheerleader is a structure: ;;(make-cheerleader n d)
;; wheren
is a symbol andd
is a list of symbols.
For example, (make-cheerleader 'JoAnn '(Carl Bob Dude Adam Emil))
is a valid cheerleader
record. Develop an example that shows what
it means to add 'Frank
as a date.
Solution
Exercise 41.2.5.
Recall the structure definitions for square
s:
(define-struct square (nw length))
The matching data definition specifies that the nw
field is always
a posn
structure and that length
is a number:
(make-square p s)
p
is a posn
and s
is a number.
Develop the function move-square!
. It consumes a square,
called sq
, and a number, called delta
. It modifies
sq
by adding delta
to its x coordinate.
Look up the structure and data definition for circles and develop the
function move-circle
, which is analogous to
move-square
.
Solution
The second example recalls the design recipe for functions that work on unions of classes. One of our first examples of this kind concerned the class of geometric shapes. Here is the relevant data definition:
a circle
, or
a square
.
See exercise 41.2.5 or part I for
the definitions of circle
and square
.
Following our recipe, a template for shape
-processing functions
consists of a two-clause cond-expression:
(define (fun-for-shape a-shape) (cond [(circle? a-shape) ... (fun-for-circle a-shape) ...] [(square? a-shape) ... (fun-for-square a-shape) ...]))
Each cond
-clause refers to a function with the same purpose for
the matching kind of shape.
So, suppose we wish to move a shape
in the x direction by
a fixed number of pixels. In part I, we created a new
structure for this purpose. Now we can use the mutators for
circle
and square
structures instead -- that is, the
function can have an effect:
;;move-shape! : shape number -> void
;; effect: to movea-shape
in the x direction bydelta
pixels (define (move-shape! a-shape) (cond [(circle? a-shape) (move-circle a-shape delta)] [(square? a-shape) (move-square a-shape delta)]))
The functions move-circle
and move-square
are the subject
of execise 41.2.5 because they consume and affect plain
structures.
Exercise 41.2.6.
Make up examples for move-shape!
and test the function. Formulate
the tests as boolean-valued expressions!
Solution
Exercise 41.2.7. The following structure definitions are to represent items that a music store sells:
(define-struct CD (price title artist)) (define-struct record (price antique title artist)) (define-struct DVD (price title artist to-appear)) (define-struct tape (price title artist))
Provide a data definition for the class of music items,
which comprises cd
s, record
s, dvd
s, and
tape
s. The price must be a number in each case.
Develop the program inflate!
, which consumes a music-item
and a percentage. Its effect is to increase the price in the given
structure according to the percentage.
Solution
Exercise 41.2.8.
Develop a program that keeps track of the feeding of zoo animals. Our zoo
has three kinds of animals: elephants, monkeys, and spiders. Each animal
has a name and two feeding times per day: morning and evening. Initially a
structure that represents an animal (structure) contains false
in
the fields for feeding times. The program feed-animal
should
consume a structure that represents an animal and the name of a feeding
time. It should switch the corresponding field in the animal structure to
true
.
Solution
The next two examples are about mutations when the underlying data definitions involve self-references. Self-references are needed if we wish to deal with data that has no size limit. Lists were the first class of such data we encountered and natural numbers the second one.
Let's first take a look at mutation of lists of structures, using the running data example of this part: the address book. An address book is a list of entries; for completeness, here are the structure and data definitions:
(define-struct entry (name number))
(make-entry n p)
n
is a symbol and p
is a number.
the empty list, empty
, or
(cons an-e an-ab)
where an-e
is an entry and
an-ab
is an address book.
Only the second one is self-referential, so we focus on the template for it:
;; fun-for-ab : address-book -> XYZ
(define (fun-for-ab ab)
(cond
[(empty? ab) ...]
[else ... (fun-for-entry (first ab)) ... (fun-for-ab (rest ab)) ...]))
If we needed an auxiliary function for processing an entry
, we
might also wish to write out the template for structure-processing
functions.
So suppose we want a function that updates an existing entry. The function
consumes an address-book
, a name, and a phone number. The first
entry
that contains the name is modified to contain the new phone
number:
;;change-number! : symbol number address-book -> void
;; effect: to modify the firstentry
forname
inab
so that its ;; number field isphone
(define (change-number! name phone ab) ...)
It is justified to develop this function with mutators because just as in reality, most of the address book stays the same while one entry is changed.
Here is an example:
(define ab (list (make-entry 'Adam 1) (make-entry 'Chris 3) (make-entry 'Eve 2))) (begin (change-number! 'Chris 17 ab) (= (entry-number (second ab)) 17))
The definition introduces ab
, an address-book
with three
items. The begin-expression first changes ab
by
associating 'Chris
with 17
; then it compares the phone
number of the second item on ab
with 17
. If
change-number!
functions properly, the result of the
begin-expression is true
. An even better test would ensure that nothing
else in ab
changes.
The next step is to develop the function definition, using the template and the examples. Let's consider each case separately:
If ab
is empty, name
doesn't occur in it. Unfortunately,
the purpose statement doesn't specify what the function should compute in this
case, and there is indeed nothing sensible the function can do. To be safe,
we use error
to signal that no matching entry was found.
If ab
contains a first entry
, it might or might not
contain name
. To find out, the function must distinguish the two
cases with a cond-expression:
(cond [(symbol=? (entry-name (first ab)) name) ...] [else ...])
In the first subcase, the function must modify the structure. In the
second, name
can occur only in (rest ab)
, which means the
function must mutate some entry
in the rest of the
list. Fortunately, the natural recursion accomplishes just that.
Putting everything together, we get the following definition:
(define (change-number! name phone ab) (cond [(empty? ab) (error 'change-number! "name not in list")] [else (cond [(symbol=? (entry-name (first ab)) name) (set-entry-number! (first ab) phone)] [else (change-number! name phone (rest ab))])]))
The only unique aspect of this function is that it uses a structure mutator
in one of the cases. Otherwise it has the familiar recursive shape: a
cond
with two clauses and a natural recursion. It is especially
instructive to compare the function with contains-doll?
from
section 9.3 and contains?
from
exercise 9.3.3.
Exercise 41.2.9.
Define test-change-number
. The function consumes a name, a phone
number, and an address book. It uses change-number!
to update the
address book, and then ensures that it was changed properly. If so, it produces
true
; if not, it produces an error message. Use this new function to
test change-number!
with at least three different
examples.
Solution
Exercise 41.2.10.
Develop move-squares
. It consumes a list of square
s, as
defined above, and a number delta
. The function modifies each on
the list by adding delta
to the x-component of its
position.
Solution
Exercise 41.2.11.
Develop the function all-fed
. It consumes a list of
animal
s, as defined in exercise 41.2.8, and modifies
them so that their field for morning feedings is switched to
true
.
Solution
Exercise 41.2.12.
Develop the function for-all
, which abstracts
move-squares
and all-fed
from exercises 41.2.10
and 41.2.11. It consumes two values: a function that consumes
structures and produces (void)
; and a list of structures. Its
result is (void)
.
Solution
Exercise 41.2.13.
Develop the function ft-descendants
. It consumes a descendant
family tree (see section 15.1) based on the following
structure definition:
(define-struct parent (children name date eyes no-descendants))
The last field in a parent
structure is originally 0
.
The function ft-descendants
traverses the tree and modifies these
slots so that they contain the total number of descendants of the
corresponding family member. Its result is the number of total descendants
of the given tree.
Solution
Natural numbers make up another class of values that requires a self-referential description. Recursion on natural numbers per se isn't useful in conjunction with mutation, but recursion on natural numbers as indices into vectors is useful when a problem's data representation involves vectors.
Let's start with a snippet of an elevator control program. An elevator
control program must know at which floor people have pressed the call
buttons. We assume that the elevator hardware can mutate some status vector
of booleans. That is, we assume that the program contains a vector, call it
call-status
, and that a field in call-status
is
true
if someone has pushed the call button at the corresponding
floor.
One important elevator operation is to reset
all the buttons. For
example, an operator may have to restart the elevator after it has been out
of service for a while. We start the development of reset
by
restating the known facts in a Scheme outline:76
;;call-status : (vectorof boolean)
;; to keep track of the floors from which calls have been issued (define call-status (vector true true true false true true true false)) ;;reset : -> void
;; effect: to set all fields incall-status
tofalse
(define (reset) ...)
The first definition specifies call-status
as a state variable,
but of course we use each slot in the vector as a state value, not the
entire variable. The second part consists of three pieces: a contract, an
effect statement, and a header for the function reset
, which
implements the informally specified service.
While it is possible to implement the service as
(define (reset) (set! call-status (build-vector (vector-length call-status) (lambda (i) false))))
this trivial solution is clearly not what we want because it creates a new vector. Instead, we want a function that modifies each field of the existing vector. Following the suggestions of intermezzo 5, we develop an auxiliary function with the following template:
;;reset-aux : (vectorof boolean) N -> void
;; effect: to set the fields ofv
with index in [0
,i
) tofalse
(define (reset-aux v i) (cond [(zero? i) ...] [else ... (reset-aux v (sub1 i)) ...]))
That is, the auxiliary function consumes not only the vector but also an interval bound. The shape of the template is based on the data definition of the latter.
The effect statement suggests the following examples:
(reset-aux call-status 0)
leaves call-status
unchanged, because the purpose statement says to change all indices in
[0
,0
) and there are none;
(reset-aux 1)
changes call-status
so that
(vector-ref call-status 0)
is false
, because 0
is the only natural number in [0
, 1
);
(reset-aux call-status (vector-length call-status))
sets all
fields of call-status
to false
.
The last example implies that we can define reset
with
(reset-aux call-status (vector-length call-status))
.
Equipped with examples, we can turn our attention to the definition. The key is to remember that the additional argument must be interpreted as an index into the vector. Keeping the example and the guideline in mind, let's look at each of the two cases separately:
If (zero? i)
holds, the function has no effect and produces
(void)
.
Otherwise i
is positive. In that case, the natural recursion
sets all fields in call-status
with an index in [0,(sub1
i)
) to false
. Furthermore, to complete the task, the function
must set the vector field with index (sub1 i)
to false
.
The combination of the two effects is achieved with a
begin-expression that sequences the natural recursion and the
additional vector-set!
.
Figure 118 puts everything together. The second clause in the
definition of reset-aux
changes the vector at index (sub1
i)
and then uses the natural recursion. Its result is the result of the
begin-expression.
Exercise 41.2.14.
Use the examples to develop tests for reset-aux
. Formulate the
tests as boolean-valued expressions.
Solution
Exercise 41.2.15.
Develop the following variant of reset
:
;;reset-interval : N N -> void
;; effect: to set all fields in [from
,to
] tofalse
;; assume:(<= from to)
holds (define (reset-interval from to) ...)
Use reset-interval
to define reset
.
Solution
Exercise 41.2.16.
Suppose we represent the position of an object with a vector and the
velocity of an object with a second vector. Develop the function
move!
, which consumes a position vector and an equally long
velocity vector. It modifies the position vector by adding in the numbers
of the speed vector, field by field:
;;move! : (vectorof number) (vectorof number) -> void
;; effect: to add the fields ofv
to the corresponding fields ofpos
;; ;; assumption:pos
andv
have equal length (define (move! pos v) ...)
Justify why the use of a vector-modifying function is appropriate to model the movement of an object. Solution
Exercise 41.2.17.
Develop the function vec-for-all
, which abstracts
reset-aux
and the auxiliary vector-processing function for
move!
from exercise 41.2.16. It consumes two values: a
function f
and a vector vec
. The function f
consumes indices (N
) and vector items. The result of
vec-for-all
is (void)
; its effect is to apply
f
to each index and corresponding value in vec
:
;;vec-for-all : (N X -> void) (vectorof X) -> void
;; effect: to applyf
to all indices and values invec
;; equation: ;;(vec-for-all f (vector v-0 ... v-N))
;; = ;;(begin (f N v-N) ... (f 0 v-0) (void))
(define (vec-for-all f vec) ...)
Use vec-for-all
to define vector*!
, which consumes a
number s
and a vector of numbers and modifies the vector by
multiplying each field's value with s
.
Solution
The last example covers the common situation when we wish to compute several numeric values at once and place them in a vector. In section 37 we saw that the use of effects is on occasion useful to communicate several results. In the same manner, it is sometimes best to create a vector and to modify it within the same function. Consider the problem of counting how many times each vowel occurs in a list of letters:
;;count-vowels : (listof letter)
;;-> (vector number number number number number)
;; where a letter is a symbol in'a ... 'z
;; to determine how many times the five vowels occur inchars
;; the resulting vector lists the counts in the lexicographic order (define (count-vowels chars) ...)
The choice of vector as a result is appropriate because the function must combine five values into one and each of the values is equally interesting.
Using the purpose statement, we can also come up with examples:
(count-vowels '(a b c d e f g h i)) = (vector 1 1 1 0 0) (count-vowels '(a a i u u)) = (vector 2 0 1 0 2)
Given that the input is a list, the natural choice for the template is that for a list-processing function:
(define (count-vowels chars) (cond [(empty? chars) ...] [else ... (first chars) ... (count-vowels (rest chars)) ... ]))
|
To fill the gaps in the template, we consider each of the two clauses:
If (empty? chars)
is true
, the result is a vector
of five 0
's. After all, there are no vowels in an empty list.
If chars
isn't empty
, the natural recursion counts
how many vowels and which ones occur in (rest chars)
. To get the
correct result, we also have to check whether (first chars)
is a
vowel, and depending on the outcome, increase one of the vector
fields. Since this kind of task is a separate, repeated task, we leave it
to an auxiliary function:
;;count-a-vowel : letter
;;(vector number number number number number) -> void
;; effect: to modifycounts
at the appropriate place ifl
is a vowel, (define (count-a-vowel l counts) ...)
In other words, the second clause first counts the vowels in the rest of
the list. This computation is guaranteed to yield a vector according to the
purpose statement. Let's call this vector counts
. Then, it uses
count-a-vowel
to increase the appropriate field in
counts
, if any. The result is counts
, after the first
letter on the list has been counted.
Figure 119 contains the complete definition of the main function. Defining the auxiliary function follows the recipe for non-recursive structure mutations.
Exercise 41.2.18.
Develop the function count-a-vowel
. Then test the complete
count-vowels
program.
Solution
Exercise 41.2.19.
At the end of intermezzo 5, we could have defined count-vowels
as
shown in figure 120. This version does not use
vector-set!
, but constructs the vector directly using
build-vector
.
|
Measure the performance difference between count-vowels-bv
and
count-vowels
. Hint: Define a function that generates a large list
of random letters (with, say, 5,000 or 10,000 items).
Explain the performance difference between count-vowels-bv
and
count-vowels
. Does the explanation reflect the measured
difference? What does this suggest concerning the vector-set!
operation?
Solution
Exercise 41.2.20.
Develop histogram
. The function consumes a list of grades between
0 and 100; it produces a vector of size 101 where each slot contains the
number of grades at that level.
Solution
Exercise 41.2.21.
Develop count-children
. The function consumes a descendant family
tree, which is a family tree that leads from a family member to the
descendants. It produces a vector with six fields. The first five slots
contain the number of family members that have that many children; the
sixth field contains the number of family members that have five or more
children.
Solution
In the preceding sections, we studied structure mutation for fields that contain atomic data. We know, however, that structures can contain structures. Starting in section 14.1, we even encountered self-referential data definitions involving structures in structures. On occasion, processing such classes of data may also require mutations of structure fields that contain structures. In this section, we work through one such example.
|
Suppose we wish to simulate a card game as a program. Each card has two important characteristics: its suit and its rank. A player's collection of cards is called a hand. For now we also assume that every player has at least one card, that is, a hand is never empty.
Figure 121 contains a screen shot of DrScheme with structure
and data definitions for manipulating cards and hands. The program fragment
does not introduce separate classes of cards and hands, but a single
structure and a single data definition for hands. A hand consists of a
hand
structure, which contains a rank
, a suit
,
and a next
field. The data definition shows that the next field
may contain two kinds of values: empty
, which means that there are
no other cards, and a hand
structure, which contains the remainder
of the cards. From a global perspective, a hand
forms a chain of
cards; only the last one contains empty
in the next
field.77
At first, a player has no cards. Picking up the first card creates a hand. Others cards are then inserted into the existing hand as needed. This calls for two functions: one for creating a hand and another one for inserting a card into a hand. Because a hand exists only once and corresponds to a physical object, it is natural to think of the second function as one that modifies an existing value rather than building a new one. For now, we accept this premise and explore its consequences.
Creating a hand is a simple act and easy to implement as a function:
;;create-hand : rank suit -> hand
;; to create a single-card hand fromr
ands
(define (create-hand r s) (make-hand r s empty))
The function consumes the properties of a card; it creates a hand with one card and no others.
|