Matrices

Section 9.4 of our textbook develops an implementation, in Scheme, of the matrix data structure, which stores data that is naturally ``two-dimensional,'' in the sense that we want to be able to extract components of the structure that are related in either of two independent ways. Our textbook's initial example involves a data base that contains the names, addresses, and telephone numbers of several different people. On one hand, we want to be able to extract all of the information about one person; on other occasions, however, we want to be able to extract all of the telephone numbers or all of the names.

If we think of the data as being laid out in a rectangular table, thus --

Geronne Arrurriza      1002 Counterfeit Road     555-3460
Lloyd Esplenalp        312 Sham Avenue           555-9232
Harl K. Frad           2119 Humbug Boulevard     555-6938
Francis Frenoolian     816 Bogus Street          555-6947
Ethel Whammo           3332 Fakename Drive       555-5558

then the two independent ways of accessing the data are ``by rows'' and ``by columns.'' A matrix is just such a grid of rows and columns.

``Primitive'' matrix procedures

The designers of Scheme decided not to include matrices as a built-in type, supported by primitive procedures for constructing them and accessing their elements. However, as programmers, we can remedy this defect by devising a way of using the data types that Scheme supports directly to represent matrices. We can then write the ``primitive'' matrix procedures ourselves, translating each such procedure into operations on the representations. Once we have completed these pseudo-primitives, we can save them in a file somewhere. In the future, whenever we have a programming problem for which matrices are appropriate, we can simply load in this file and proceed to write our solution using the matrix operations, just as if it were a directly supported type.

As their ``primitives,'' the authors chose the following matrix operations:

Choosing a representation

The authors choose to represent a matrix with m rows and n columns as a vector containing mn + 1 elements. The first n elements of this underlying vector make up the first row of the matrix that it represents, the next n elements the second row, and so on; as the book mentions, this way of linearizing a two-dimensional structures is called ``row-major order.'' The leftover element at the end is used to store the value n itself in a conveniently accessible way.

This implementation is reasonably efficient, but it doesn't handle exceptional cases well (for instance, Program 9.31 crashes if it is given a null matrix that has zero columns), and it doesn't generalize easily to matrix-like data structures of three or more dimensions (although exercise 9.14 indicates how such a generalization could be done). As an alternative, let's develop a different representation and rewrite the primitives to reflect the difference.

We'll represent a matrix as a pair in which the car specifies the ``shape'' of the matrix -- that is, its dimensions -- and the cdr specifies its contents -- the actual values stored in the cells of the matrix. The shape will be a list of two natural numbers, the first giving the number of rows, m, and the second the number of columns, n. The contents will be a vector of size mn, containing all of the matrix elements, laid out in row-major order.

The procedures for determining the number of rows and the number of columns in a matrix are now just selectors that look at the correct position in the data structure:

(define num-rows caar)
(define num-cols cadar)

To generate a matrix using a specified element-generation procedure, once we are given the number of rows and the number of columns, we can use the vector-generator procedure from the lab on vectors to construct the contents vector:

(define matrix-generator
  (lambda (matrix-element-maker)
    (lambda (number-of-rows number-of-columns)
      (let ((vector-element-maker
             (lambda (index)
               (matrix-element-maker (quotient index number-of-columns)
                                     (remainder index number-of-columns)))))
        (cons (list number-of-rows number-of-columns)
              ((vector-generator vector-element-maker)
               (* number-of-rows number-of-columns)))))))

The (zero-based) row and column indices of a matrix element are computed by dividing the (zero-based) index of the contents vector by the number of columns in the matrix. The quotient represents the number of complete rows above the row containing the specified matrix element; the remainder represents the number of values preceding the specified matrix element in its row.

To recover an element from a matrix, once we're given its row and column indices, we perform the converse arithmetic operation to recover the index into the contents vector. Specifically, we multiply the row index by the number of columns, then add in the column index:

(define matrix-ref
  (lambda (mat)
    (let ((number-of-columns (num-cols mat)))
      (lambda (row-index column-index)
        (vector-ref (cdr mat)
                    (+ (* row-index number-of-columns) column-index))))))

The matrix-set! procedure uses the same technique:

(define matrix-set!
  (lambda (mat)
    (let ((number-of-columns (num-cols mat)))
      (lambda (row-index column-index new-value)
        (vector-set! (cdr mat)
                     (+ (* row-index number-of-columns) column-index)
                     new-value)))))

Applying the primitives

In section 9.4 of the text, the authors proceeded to develop several interesting operations on matrices, such as row-of, column-of, matrix-transpose, and (for matrices in which all of the elements are numbers) matrix-product. Because these procedures invoke the pseudo-primitives defined above, rather than operating directly on the underlying representations, we can use the authors' code without change, even though the low-level computations are quite different.

Cleanly separating the implementation of the primitive operations on some type of values or objects from the applications in which such values or objects are used is an important part of developing large-scale programs. Like procedure abstraction, it allows a programmer to address separate subproblems independently, instead of constantly having to consider their interconnections and interactions. Moreover, when several programmers are working together on a large project, the ones who are fine-tuning the implementation can operate almost independently of the ones who are developing the application, provided only that they agree on the interface -- the parameter lists, preconditions, return values, and postconditions of the pseudo-primitive procedures.


Exercise 1

Make your own copy of the procedures that make up our implementation of matrices by giving the command

cp /home/stone/courses/scheme/examples/matrices.ss matrices.ss

in your terminal emulator. Start DrScheme and open the matrices.ss file.

Use matrix-generator to construct the matrix

 0   1   2   3
 4   5   6   7
 8   9  10  11
12  13  14  15

in which each entry is the sum of its column index and four times its row index. Define sample-matrix to be this matrix.

Use matrix-set! to replace the element in row 2, column 0 with -1.


Exercise 2

Whether you use the authors' implementation of matrices or the one developed here, Scheme's display procedure exposes the underlying representation of matrix objects instead of printing them in the rectangular-grid format that one might prefer. Define and test a display-matrix procedure that takes a matrix as its argument and prints it out, one row per line, with a space between any two successive elements of a row:

> (display-matrix fractions)
0 0 0 0 0
1 1/2 1/3 1/4 1/5
2 1 2/3 1/2 2/5
3 3/2 1 3/4 3/5

Use the pseudo-primitive operations; do not access the underlying representation directly.


Exercise 3

Define and test a procedure column-swapper that takes a matrix as its argument and returns a ``swapper'' procedure. The swapper should take two natural numbers as arguments, interpreting each one as a column index for the given matrix, and (as a side effect) exchange all the entries in the specified columns of the matrix.

> (define sample ((matrix-generator (lambda (row col)
                                      (- (* row row) (* col col))))
                  6 6))
> (display-matrix sample)
0 -1 -4 -9 -16 -25
1 0 -3 -8 -15 -24
4 3 0 -5 -12 -21
9 8 5 0 -7 -16
16 15 12 7 0 -9
25 24 21 16 9 0
> (define swapper! (column-swapper sample))
> (swapper! 2 4)
> (display-matrix sample)
0 -1 -16 -9 -4 -25
1 0 -15 -8 -3 -24
4 3 -12 -5 0 -21
9 8 -7 0 5 -16
16 15 0 7 12 -9
25 24 9 16 21 0

Exercise 4

Define and test a procedure that finds the sum of two given matrices of numbers. It is a precondition of this procedure that the two matrices have the same dimensions. The sum is yet another procedure of the same dimensions, in which each element is the sum of the elements in the corresponding position (that is, with the same row and column indices) in the given matrices.


Exercise 5

If you were designing a three-dimensional array structure, which arranges data in three independently accessible orders (in rows, columns, and planes, for instance), what primitives would you provide? What data structure would you use to represent such an array? How would you implement the primitives?


This document is available on the World Wide Web as

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

created April 11, 2000
last revised April 13, 2000

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