Structure mutation

In the lab on vectors, we developed some procedures that took vectors as arguments and constructed and returned other vectors as results -- for instance, the double-every-element procedure:

(define double-every-element
  (lambda (vec)
    (let* ((size (vector-length vec))
           (result (make-vector size)))
      (let loop ((position 0))
        (if (= position size)
            result
            (begin
              (vector-set! result position (* 2 (vector-ref vec position)))
              (loop (+ position 1))))))))

Instead of constructing an entirely new vector, it is often more efficient to replace the elements of the original vector with the newly computed elements:

(define double-every-element!
  (lambda (vec)
    (let ((size (vector-length vec)))
      (let loop ((position 0))
        (if (< position size)
            (begin
              (vector-set! vec position (* 2 (vector-ref vec position)))
              (loop (+ position 1))))))))

> (define sample-vector (vector 3 1 4 1 5 9))

> sample-vector
#(3 1 4 1 5 9)

> (double-every-element! sample-vector)

> sample-vector
#(6 2 8 2 10 18)

As the exclamation point at the end of double-every-element! indicates, calling the procedure causes an irreversible change of state in its argument, sample-vector. The original contents of that vector are gone; each of the original elements has been replaced by its double. This ``destructive'' procedure is more efficient than double-every-element, because it is unnecessary to allocate storage for a result vector, but it is also somewhat trickier to use. The timing is important: one must not replace the elements of the vector too soon, at a time when one still needs some of the old values for other computations.

This pattern of computation is common enough that it is useful to define a destructive version of vector-map:

(define vector-map!
  (lambda (proc vec)
    (let ((size (vector-length vec)))
      (let loop ((position 0))
        (if (< position size)
            (begin
              (vector-set! vec position (proc (vector-ref vec position)))
              (loop (+ position 1))))))))

> (define sample-vector (vector 3 1 4 1 5 9))

> (vector-map! (lambda (n) (* n n)) sample-vector)

> sample-vector
#(9 1 16 1 25 81)

Exercise 1

Using vector-map!, define a Scheme procedure convert-negatives-to-zero! that takes any vector of real numbers as its argument and, as a side effect, replaces any negative value in that vector with 0.

> (define example (vector -3.8 17 0.14 -0.14 -113/4))

> (convert-negatives-to-zero! example)

> example
#(0 17 0.14 0 0)

Another way of using the vector-set! procedure is illustrated by our implementation of the vector-generator procedure:

(define vector-generator
  (lambda (proc)
    (lambda (size)
      (let ((result (make-vector size)))
        (let loop ((position 0))
          (if (= position size)
              result
              (begin
                (vector-set! result position (proc position))
                (loop (+ position 1)))))))))

> ((vector-generator (lambda (x) (+ x 1))) 6)
#(1 2 3 4 5 6)

Exercise 2

Using vector-generator, define a procedure factorial-table that takes any natural number as its argument and produces a vector of the specified length, each element of which is the factorial of its position number.

> (factorial-table 8)
#(1 1 2 6 24 120 720 5040)

Exercise 3

An accumulation procedure is one that traverses a vector from left to right, replacing each element with the result of performing some given operation on all of the elements up to and including that one. For example, here is a procedure that accumulates partial sums in a vector of numbers:

(define accumulate-sum!
  (lambda (vec)
    (let ((size (vector-length vec)))
      (let loop ((position 0)
                 (sum 0))
        (if (< position size)
            (let ((new-sum (+ sum (vector-ref vec position))))
              (vector-set! vec position new-sum)
              (loop (+ position 1) new-sum))))))))

> (define sample-vector (vector 3 1 4 1 5 9))

> (accumulate-sum! sample-vector)

> sample-vector
#(3 4 8 9 14 23)

And here is one that accumulates strings by concatenation:

(define accumulate-strings!
  (lambda (vec)
    (let ((size (vector-length vec)))
      (let loop ((position 0)
                 (so-far ""))
        (if (< position size)
            (let ((new-value (string-append so-far
                                            (vector-ref vec position))))
              (vector-set! vec position new-value)
              (loop (+ position 1) new-value))))))))

> (define string-vector (vector "To " "be " "or " "not " "to " "be "))

> (accumulate-strings! string-vector)

> string-vector
#("To "
  "To be "
  "To be or "
  "To be or not "
  "To be or not to "
  "To be or not to be ")

Define a Scheme procedure accumulate! that takes two arguments -- a procedure proc of arity 2 and a ``seed value'' seed -- and returns an accumulated procedure that, when invoked, applies proc across a given vector, starting by applying it to seed and the initial element of the vector, then to the result of the first application and the next element of the vector, and so on. Like the accumulation procedures above, the one returned by accumulate! should, as a side effect, replace all the elements of the vector it is given with the intermediate results.

(define accumulate-sum!
  (accumulate! + 0))

(define accumulate-strings!
  (accumulate! string-append ""))

> (define powers (make-vector 12 1))

> ((accumulate! (lambda (x y) (* 2 x y)) 1) powers)

> powers
#(2 4 8 16 32 64 128 256 512 1024 2048 4096)

Other built-in Scheme data structures have mutator procedures as well:

The set-cdr! procedure in particular should be used with great caution, since it is all too easy to create (by accident) a data structure for which the box-and-pointer diagram contains a cycle -- for instance, a structure in which box A contains a pointer to box B, which contains a pointer back to box A. List-traversal procedures often enter infinite loops when confronted with such a structure.


Exercise 4

Standard Scheme does not supply a list-set! procedure, for replacing the element in a specified position in a given list. Define one, using set-car!.


This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/courses/scheme/structure-mutation.html

created November 8, 1997
last revised April 6, 1998

John David Stone (stone@math.grin.edu)