Now that our tool box contains structure mutation, we'll be seeing more and more cases in which it is useful to perform some procedure call or sequence of procedure calls repeatedly, for the sake of the side effects on structures or for input or output. For example, the iterative versions of the sorting algorithms that we'll study in a few more days involve repeated mutations on the vectors to be sorted.
Most of these iterative constructions have a common form, and Scheme
provides a special expression type to capture this form concisely and
efficiently: the do-expression. A do-expression
has the following structure:
(do loop-control-list
(exit-test postlude)
body)
Let's consider each part in turn:
The loop control list sets up bindings for any local variables
that the do-expression will use. Generally, there is at least one
``loop control variable'' that counts off repetitions of the loop or
positions in a vector or some such thing. A loop control list is a list of
zero or more loop control specifications, one for each variable
that is to be bound locally. A loop control specification is a list in
which
let-expression),
The initializing expression is evaluated only once, when the
do-expression is entered for the first time. The updating
expression may be evaluated any number of times.
The exit test is a single expression that is evaluated to
determine when enough iterations have been performed; the loop is finished
as soon as the value of the exit test is anything other than #f.
The postlude, which is optional, is a sequence of expressions to
be evaluated after the exit test has succeeded; the value of the last of
these expressions becomes the value of the entire do-expression. (If
the postlude is omitted, the value of the do-expression is
unspecified and the presumption is that the expression is being evaluated
for its side effect.)
The body, which is also optional, is a sequence of expressions to be evaluated, in order, on each iteration. All of the expressions in the body are evaluated only for their side effects; the values are discarded.
As a simple example of a do-expression, let's look at a procedure
that destructively ``rotates'' a non-empty vector, moving its last element
to the initial position and shifting every other element to the next higher
position:
;;; rotate-vector!: destructively move each element of a vector to the ;;; next higher position (and the last element to the initial position) ;;; Given: ;;; VEC, a vector. ;;; Results: ;;; None. ;;; Precondition: ;;; The length of VEC is not zero. ;;; Postconditions: ;;; Let LEN be the length of VEC. Then: ;;; (1) The value at position 0 of VEC is the value that was originally ;;; at position LEN - 1. ;;; (2) For every natural number k less than LEN - 1, the value at ;;; position k + 1 of VEC is the value that was originally at ;;; position k. (define rotate-vector! (lambda (vec) (let ((len (vector-length vec))) (let ((skipper (vector-ref vec (- len 1)))) (do ((position (- len 1) (- position 1))) ((zero? position) (vector-set! vec position skipper)) (vector-set! vec position (vector-ref vec (- position 1))))))))
The effect of the procedure is demonstrated in the following interactions:
> (define animals (vector 'cat 'dog 'fish)) > (rotate-vector! animals) > animals #(fish cat dog)
Let's step through the execution of the do-expression during the
call to rotate-vector!. When we enter it, we have already bound
len to 3 and skipper to the symbol fish. Then:
The do-expression has just one loop control variable in this case,
namely position. The first thing that happens is that a local
binding for position is created, with the initial value of (-
len 1), which is 2. The updating expression (- position 1) is
ignored at this point, because we haven't completed any iterations of the
loop yet.
Next, the exit test, (zero? position), is evaluated. Since 2
is not zero, the exit is not taken.
Next, the body of the loop is evaluated. The body in this case
consists is the procedure call to vector-set!. DrScheme computes
(- position 1), getting 1; calls vector-ref to extract the
item at position 1 of vec, which is dog; and puts this item
right back into vec at position 2. At this point the value of vec is #(cat dog dog), so we're clearly not done yet.
Now the updating expression for the loop control variable is evaluated.
The value of (- position 1) is 1; this becomes the new value
of position for the next iteration.
Next, the exit test is evaluated again. 1 is not zero, so the exit is
not taken; instead, the loop body is evaluated again. This time the value
of (- position 1) is 0, so the element at position 0, cat, is
copied into position 1, overwriting the symbol that was there before. This
completes the second iteration. The value of vec at this point is
#(cat cat dog).
The loop control variable position is updated again,
changing from 1 to 0. When the exit test is evaluated this time, its value
is #t, so the loop is finished.
Now the postlude is evaluated. In this case, the postlude is another call
to vector-set!; this one stores the value of skipper, which
is fish, in position 0 of vec. The value of vec is
now #(fish cat dog), as required. The (undefined and useless) value
of the call to vector-set! is returned as the value of the do-expression, and hence the value of the procedure call; that's all
right, because we invoke rotate-vector! only for its side effect,
not because we want a value back from it.
Let's look at two more examples of the use of do-expressions:
first, a procedure that takes as arguments two vectors of equal length
containing numbers and returns a similar vector in which the elements are
the sums of the corresponding elements of the argument vectors:
;;; vector+: add two vectors componentwise ;;; Givens: ;;; AUGEND and ADDEND, both vectors of numbers ;;; Result: ;;; SUM, a vector ;;; Precondition: ;;; The length of AUGEND is equal to the length of ADDEND. ;;; Postconditions: ;;; (1) The length of SUM is equal to the length of AUGEND and ADDEND. ;;; (2) For every natural number k less than the length of SUM, the ;;; element at position k of SUM is the sum of the elements at ;;; position k in AUGEND and ADDEND. (define vector+ (lambda (augend addend) (let* ((len (vector-length augend)) (result (make-vector len))) (do ((position 0 (+ position 1))) ((= position len) result) (vector-set! result position (+ (vector-ref augend position) (vector-ref addend position)))))))
> (vector+ '#(3 1 4 1 5 9) '#(2 7 1 8 2 8)) #(5 8 5 9 7 17)
Second, here is a version of the vector-reverse! procedure
that uses a do-expression. The vector-reverse! procedure
takes any vector as its argument and destructively rearranges its elements
into the reverse order:
;;; vector-reverse!: destructively reverse the order of the elements ;;; in a given vector ;;; Given: ;;; VEC, a vector. ;;; Results: ;;; None. ;;; Preconditions: ;;; None. ;;; Postcondition: ;;; Let LEN be the length of VEC. Then, for every natural number k ;;; less than LEN, the element at position k of VEC is the element ;;; that was initially at position LEN - 1 - k. (define vector-reverse! (lambda (vec) (do ((left-index 0 (+ left-index 1)) (right-index (- (vector-length vec) 1) (- right-index 1))) ((<= right-index left-index)) (let ((temp (vector-ref vec left-index))) (vector-set! vec left-index (vector-ref vec right-index)) (vector-set! vec right-index temp)))))
This time there are two loop-control variables: left-index
starts with the lowest-numbered position of the vector and increases by 1
on each iteration, and right-index starts with the
highest-numbered position (one less than the length of the vector) and
decreases by 1 on each iteration. The iteration stops when
right-index is less than or equal to left-index
(because the indices have met or crossed in the middle of the vector). On
each iteration, the body of the do-expression swaps the
elements in the positions marked by the indices. Since the postlude is
omitted, vector-reverse! does not reliably return any useful value;
it is invoked only for its side effect.
> (define sample (vector 0 1 2 3 4 5 6)) > sample #(0 1 2 3 4 5 6) > (vector-reverse! sample) > sample #(6 5 4 3 2 1 0)
Finally, here is yet another version of the sum procedure,
which takes any list of numbers as its argument and returns their sum:
;;; sum: find the sum of the numbers in a given list ;;; Given: ;;; LS, a list of exact numbers. ;;; Result: ;;; TOTAL, an exact number. ;;; Preconditions: ;;; None. ;;; Postconditions: ;;; TOTAL is the sum of all of the elements of LS, ;;; and is 0 if LS has no elements. (define sum (lambda (ls) (do ((rest ls (cdr rest)) (so-far 0 (+ so-far (car rest)))) ((null? rest) so-far))))
In this example, there are two loop-control variables, rest and
so-far. The variable rest is initially the entire list of
numbers; but it is changed on each iteration to become the cdr of its value
in the preceding iteration. So-far is initially 0; at the end of
each subsequent iteration, the first element of the old value of rest is recovered and added to the old value of so-far to yield its
new value. The iteration stops when rest has been reduced to the
null list. At that point, the final value of so-far is returned.
This do-expression has no expressions in its body! All the work is
done in the updating and exit-testing parts of the expression. This too is
a common pattern in Scheme.
This example also reveals that the loop-control variables are updated
simultaneously (as in a let-expression), not successively (as in a
let*-expression); the identifier rest in both updating
expressions refers to the old value of rest, not the new one.