Iteration

Course links

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:

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:

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.