Vectors

A vector is a data structure that contains some fixed number of elements and provides random access to them, in the sense that each element, regardless of its position in the vector, can be recovered in the same amount of time. In this respect, a vector differs from a list: The first element of a list is immediately accessible, but subsequent elements are increasingly difficult and time-consuming to get at.

In addition, a vector is a mutable data structure: It is possible to replace an element of a vector with a different value, just as one can take out the contents of a container and put in something else instead. It's still the same vector after the replacement, just as the container retains its identity no matter how often its contents are changed.

The particular values that a vector contains at some particular moment constitute its state. One could summarize the preceding paragraph by saying that the state of a vector can change and that state changes do not affect the underlying identity of the vector.

When displaying a vector, Scheme displays each of its elements, enclosed in parentheses, with an extra mesh character, #, in front of the left parenthesis. For instance, here's how Scheme displays a vector containing the symbols alpha, beta, and gamma, in that order:

#(alpha beta gamma)

The mesh character distinguishes the vector from the list containing the same elements.

We can use the same syntax to specify a vector when writing a Scheme program or typing commands and definitions into the Scheme interactive interface, except that we have to place a single quotation mark at the beginning, just as we do with list literals, so that Scheme will not try to evaluate the vector as if it were some exotic kind of procedure call. (DrScheme will not make this mistake even if you forget the single quotation mark, but not all implementations of Scheme are so generous.)

In the interaction window, when DrScheme displays a vector as the value of some top-level expression that the user supplied, it does something more ambitious, but potentially rather confusing: It inserts a numeral between the mesh character and the left parenthesis to indicate the number of elements in the vector, thus:

#3(alpha beta gamma)

However, you can instruct DrScheme to use the straight mesh-and-parentheses representation, with no numeral, by giving the following command at the beginning of your program or interactive session:

(print-vector-length #f)

-- that is, ``Don't print the lengths of vectors.'' I recommend giving this command at the beginning of each of the labs that deal with vectors.


Exercise 1

Start DrScheme and tell it not to print the lengths of vectors.


Exercise 2

In DrScheme's interaction window, type in a vector literal that denotes a vector containing just the two elements 3.14159 and 2.71828. How does DrScheme display the value of this vector?


Standard Scheme provides the following fundamental procedures for creating vectors and selecting and replacing their elements:

Some older implementations of Scheme may lack the list->vector, vector->list, and vector-fill! procedures, but it is straightforward to define them in terms of the others:

(define our-list->vector
  (lambda (ls)
    (apply vector ls)))

In other words: Call the vector procedure, giving it the elements of ls as its arguments.

(define our-vector->list
  (lambda (vec)
    (let kernel ((remaining (vector-length vec))
                 (result '()))
      (if (zero? remaining)
          result
          (let ((position (- remaining 1)))
            (kernel position (cons (vector-ref vec position) result)))))))

In other words: Let remaining initially be the number of elements in the vector. This parameter is used to keep track of how many elements of the vector remain to be transferred to the list. Let result initially be the empty list. We shall add the elements of the vector, one by one, to result. If there are no more elements to be transferred, return the finished list result; otherwise, let position be one less than remaining, so that it indicates the position in the vector that is occupied by the rightmost element that has not yet been transferred. Select that element from the vector (with vector-ref and add it to the beginning of the result list (with cons); then start over again, reducing the number of elements remaining by 1.

(define our-vector-fill!
  (lambda (vec obj)
    (let ((size (vector-length vec)))
      (let kernel ((position 0))
        (if (< position size)
            (begin
              (vector-set! vec position obj)
              (kernel (+ position 1))))))))

In other words: Let size be the number of elements in the vector vec. Starting at position 0, put obj into each position within vec, overwriting the value previously stored in that position. Increase position by 1 after each such overwriting step. When position becomes equal to size, stop. (Since the if-expression has no alternate, an unspecified value is returned at the end of the process. Since vector-fill! is invoked only for its side effect, the value it returns is insignificant.)

With these procedures, we can write all of the other vector operations described in chapter 9 of the textbook. For example, here is the vector-generator procedure:

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

Exercise 3

Write a vector-sum procedure that takes one argument, a vector of numbers, and returns the sum of the elements of that vector. (You can use our-vector->list as a pattern for vector-sum -- only a few judicious changes are needed.)


The double-every-element procedure takes one argument, a vector vec of numbers, and returns a new vector just like vec except that each of the elements is twice the corresponding element of vec.

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

> (double-every-element '#(3 1 4 1 5 9))
#(6 2 8 2 10 18)

In English: Let size be the length of the vector vec, and let result be a new vector of the same length (contents unspecified). Start at position 0. If the position number is equal to size, all of the elements of vec have already been processed; return the result vector. Otherwise, double the element stored in the current position of vec and put the doubled number into the corresponding position in result; then proceed to the next position.

An alternative definition of double-every-element uses vector-generator:

(define double-every-element
  (lambda (vec)
    ((vector-generator (compose double (left-section vector-ref vec))))
     (vector-length vec))))

Exercise 4

Write a Scheme procedure length-of-every-element that takes as argument a vector of strings and returns a vector containing the lengths of those strings.

> (length-of-every-element '#("red" "white" "and" "blue"))
#(3 5 3 4)

One can abstract the control structure that double-every-element and length-of-every-element share to obtain a general vector-map procedure, for constructing the vector that results from applying the same procedure proc to each element of a given vector vec:

(define vector-map
  (lambda (proc vec)
    (let* ((size (vector-length vec))
           (result (make-vector size)))
      (let kernel ((position 0))
        (if (= position size)
            result
            (begin
              (vector-set! result position (proc (vector-ref vec position)))
              (kernel (+ position 1))))))))

or, if one wants to be able to apply it to a procedure of arity n followed by n vectors of equal length, in the manner of the built-in map procedure for lists,

(define vector-map
  (lambda (proc first-vec . rest-of-vecs)
    (let* ((size (vector-length first-vec))
           (result (make-vector size)))
      (let ((all-vecs (cons first-vec rest-of-vecs)))
        (let kernel ((position 0))
          (if (= position size)
              result
              (begin
                (vector-set! result position
                             (apply proc (map (lambda (vec)
                                                (vector-ref vec position))
                                              all-vecs)))
                (kernel (+ position 1)))))))))

> (vector-map / '#(6 7 8 9 10) '#(1 2 3 4 5))
#(6 7/2 8/3 9/4 2)

Exercise 5

Define a similar procedure vector-for-each, which takes a procedure as its first argument and one or more vectors of equal length as its remaining arguments and applies the procedure to corresponding elements of the vectors, for the side effects only (like the built-in for-each procedure for lists), discarding the results.

> (vector-for-each display '#("con" "duct" "or"))
conductor

This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/vectors.xhtml

created November 7, 1997
last revised April 5, 2000

John David Stone (stone@cs.grinnell.edu)