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* ((len (vector-length vec))
           (result (make-vector len)))
      (let loop ((position 0))
        (if (= position len)
            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 ((len (vector-length vec)))
      (let loop ((position 0))
        (if (< position len)
            (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 ((len (vector-length vec)))
      (let loop ((position 0))
        (if (< position len)
            (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)
  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 an implementation of the vector-generator procedure, which the textbook explains as follows:

The constructor vector-generator, which we use for vectors, is a curried procedure with the call structure

((vector-generator gen-proc) size)

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 that index in the fector 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.

Standard Scheme does not provide the vector-generator procedure, but it is easily defined:

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

> ((vector-generator (lambda (x) (+ x 1))) 6)
#(1 2 3 4 5 6)
  1. 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)
    
  2. 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 ((len (vector-length vec)))
          (let loop ((position 0)
                     (sum 0))
            (if (< position len)
                (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 ((len (vector-length vec)))
          (let loop ((position 0)
                     (so-far ""))
            (if (< position len)
                (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.

  1. 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 November 13, 1997

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