Records

A record is a data structure with a fixed number of components, the fields of the record. Each field has a name, and one uses that name to access that particular field of a record. Ideally, the structure should provide ``random access'' to each field; in other words, one should be able to inspect and possibly modify the contents of any field, independently, without taking the time to access any of the others.

For instance, an astronomical database that keeps track of information about various stars might assemble the data into records, one record for each star. The fields of such a record might include name, right-ascension, declination, visual-magnitude, spectral-type, and so on.

Similarly, a library's card catalog includes a record for each book or other item that the library holds, with fields like author, title, imprint, call-number, and checked-out?.

Scheme does not provide any built-in record types, so the programmer has to define her own. Fortunately, this is straightforward. To create a new record type foo in Scheme, one should define a procedure to carry out each of the following operations:

There's an obvious analogy between this collection of procedures and the provision that Scheme makes for its built-in data structures (vector, string, and pair). In the case of string, for instance, Scheme supplies a constructor (make-string), a selector (string-ref), a mutator (string-set!), and a type predicate (string?). The only difference is that the characters in a string are accessed by position number, so that it is sufficient to provide just one selector and just one mutator; each field in a record is accessed by separate procedures, so that one must provide as many selector procedures as there are fields and as many mutator procedures as there are mutable fields.

To implement a record type in Scheme, one has to select an existing type and figure out how to perform the record operations using only values of the existing type. There are various ways to do this. One might, for instance, make each record an association list -- a list of pairs, each pair having a field name as its car and the value stored in that field as its cdr. This can be implemented elegantly in Scheme, but it does not provide random access to the field values, since it is necessary to walk down the association list one pair at a time until one arrives at the pair corresponding to the field one wishes to examine.

One common and widely recommended approach that does provide random access is to use vectors as the implementation type. A record containing n fields can be identified with a vector of n + 1 elements -- the first being a symbol identifying the record type, and the subsequent ones being the values of the various fields.

Here, for instance, is what a vector implementation of the star record type described above looks like:

;; The conventional name for a constructor is the name of the type with
;; MAKE- prefixed to it.

(define make-star
  (lambda (name right-ascension declination visual-magnitude spectral-type)
    (vector 'star name right-ascension declination visual-magnitude
            spectral-type))) 

;; The names of the selectors are conventionally formed from the name of
;; the type and the names of the respective fields.

(define star-name
  (lambda (s)
    (vector-ref s 1)))

(define star-right-ascension
  (lambda (s)
    (vector-ref s 2)))

(define star-declination
  (lambda (s)
    (vector-ref s 3)))

(define star-visual-magnitude
  (lambda (s)
    (vector-ref s 4)))

(define star-spectral-type
  (lambda (s)
    (vector-ref s 5)))

;; For demonstration purposes, we'll show all the possible mutators,
;; even though in most applications some or all of them would be omitted.

;; The conventional name of a mutator is like that of the corresponding
;; selector, but with SET- added at the beginning and an exclamation point
;; at the end. 

(define set-star-name!
  (lambda (s new-name)
    (vector-set! s 1 new-name)))

(define set-star-right-ascension!
  (lambda (s new-right-ascension)
    (vector-set! s 2 new-right-ascension)))

(define set-star-declination!
  (lambda (s new-declination)
    (vector-set! s 3 new-declination)))

(define set-star-visual-magnitude!
  (lambda (s new-visual-magnitude)
    (vector-set! s 4 new-visual-magnitude)))

(define set-star-spectral-type!
  (lambda (s new-spectral-type)
    (vector-set! s 5 new-spectral-type)))

;; Finally, it is conventional to form the name of the type predicate by
;; adding a question mark at the end of the name of the type.

(define star?
  (lambda (obj)
    (and (vector? obj)
         (= (vector-length obj) 6)
         (eq? (vector-ref obj 0) 'star))))

;; In many applications, it would be a good idea to add precondition tests
;; to all of these procedures except STAR?.

Here are some examples of the use of these procedures:

> (print-vector-length #f)
> (define Stern
    (make-star "Alpha Centauri 1" '(14 39 26) '(-60 49 25) -0.01 'G2))
> Stern
#(star "Alpha Centauri 1" (14 39 26) (-60 49 25) -0.01 g2)
> (star? Stern)
#t
> (star? (vector 'foo 'bar 'baz))
#f
> (star? 17)
#f
> (star-name Stern)
"Alpha Centauri 1"
> (star-visual-magnitude Stern)
-0.01
> (set-star-right-ascension! Stern '(14 39 26.2))
> Stern
#(star "Alpha Centauri 1" (14 39 26.2) (-60 49 25) -0.01 g2)
  1. Write a similar set of definitions for a record type shirt, to be used in a program that keeps track of the inventory of a clothing store. Provide fields for catalog number, size, color, price, and quantity in stock; only the last two fields should be mutable.

The sorting and searching methods that we have been studying are easily adapted to lists and vectors in which the elements are records, to be sorted according to the values in some field. For instance, suppose that we are given a vector of records of type star and asked to arrange the records in order of ascending visual magnitude (that is, with the brightest stars in the lowest-numbered positions). The following variant of insertion sorting does the job:

(define brighter?
  (lambda (star-1 star-2)
    (< (star-visual-magnitude star-1) (star-visual-magnitude star-2))))

(define vector-insert!
  (lambda (k vec)
    (let ((val (vector-ref vec k)))
      (let insert-h ((m k))
        (if (zero? m)
            (vector-set! vec 0 val)
            (let ((comp (vector-ref vec (sub1 m))))
              (if (brighter? val comp)
                  (begin
                    (vector-set! vec m comp)
                    (insert-h (sub1 m)))
                  (vector-set! vec m val))))))))

(define vector-insertsort!
  (lambda (v)
    (do ((size (vector-length v))
         (k 1 (+ k 1)))
        ((>= k size))
      (vector-insert! k v))))

Here the vector-insertsort! procedure is identical to the one given in the lab on insertion sorting, and the vector-insert! procedure is almost the same -- where the original vector-insert! called the < procedure to compare two numbers, the current version invokes brighter? to compare two stars.

  1. Adapt the insertion sorting procedures to sort a vector of records of type shirt by catalog number.

There are lots of real-world programs that make very extensive use of records. After writing out a few sets of definitions to implement record types, Scheme programmers generally make an important discovery: Typing out these definitions is boring. They have a completely predictable form -- only the name of the type and the number and names of the fields change from one kind of record to another -- and yet one has to sit down and type out a dozen or so procedures each time one wants to use a new kind of record. The process soon becomes tedious and error-prone.

The solution to this problem is metaprogramming: the creation of programs that construct other programs. Metaprogramming automates some of the tedious and error-prone parts of the programmer's job. Scheme is particularly well suited to metaprogramming because of the fact that definitions and commands in Scheme have the same form as data: They are simply trees in which the leaves include symbols such as lambda, if, and cons, together with literal constants of various sorts.

For instance, if we wanted to build a datum that would look just like the Scheme definition

(define square
  (lambda (n)
    (* n n)))

we could do it simply by using the list procedure to collect the right symbols in the right ways:

(list 'define 'square
      (list 'lambda (list 'n)
            (list '* 'n 'n)))
  1. Evaluate the preceding expression to confirm that its value is a datum that looks just like the definition of square.

  2. Write a Scheme expression that, when evaluated, yields a datum that looks just like the definition of the make-list-of-one procedure on page 34 of the textbook.

It's not much more difficult to metaprogram a procedure that will take as arguments a symbol that tells the name of a record type and a list of symbols that tell the names of its fields, and returns a datum that looks just like a constructor procedure:

(define constructor-maker
  (lambda (record-name field-names)
    (let ((constructor-name
           (string->symbol (string-append "make-"
                                          (symbol->string record-name)))))
      (list 'define constructor-name
            (list 'lambda field-names
                  (cons 'vector
                        (cons (list 'quote record-name) field-names)))))))

Here's what happens when you invoke it:

> (constructor-maker 'star '(name right-ascension declination
                                  visual-magnitude spectral-type))
(define make-star
  (lambda (name right-ascension declination visual-magnitude spectral-type)
    (vector 'star name right-ascension declination visual-magnitude
            spectral-type)))

If we write this datum-that-looks-like-a-definition to a file and subsequently load that file as if it contained a Scheme program, neither Chez Scheme nor any human reader can tell that it is ``only a datum''! It looks just like a definition and is in fact indistinguishable from a definition that a human programmer might have written and placed in that same file.

We can similarly metaprogram all the other components of the implementation of a record type. We can even collect them into a procedure that takes the record and field names as arguments and writes the entire collection of procedures -- constructor, selectors, mutators, type testers -- into an appropriately named file for us. So we can use this procedure to generate any record type we may want:

> (generate-record-definition-file 'star 'name 'right-ascension
                                   'declination 'visual-magnitude
                                   'spectral-type)
> (load "star-definition.ss")
> (define Stern
    (make-star "Alpha Centauri 1" '(14 39 26) '(-60 49 25) -0.01 'G2))

The file record-builder.ss contains a definition of the generate-record-definition-file procedure.

  1. Load the record-builder.ss file and use the generate-record-definition-file procedure to construct a catalog-card record type, with five fields: author, title, imprint, call-number, and checked-out?.

  2. Inspect the file that generate-record-definition-file creates (it will be called catalog-card-definition.ss). Edit this file to remove the mutators for all of the fields except checked-out?.

  3. Load the catalog-card-definition.ss file and use the constructor procedure defined in it to create a catalog card for our textbook. (In Burling library, its call number is "QA76.6.S686 1989" and it is currently checked out.)

  4. Entertaining challenge problem in metaprogramming: Write a Scheme expression (other than a literal constant) that, when evaluated, yields a datum that looks just like itself.


This document is available on the World Wide Web as

http://www.math.grin.edu/courses/Scheme/records.html

created April 29, 1997
last revised May 28, 1997
John David Stone (stone@math.grin.edu)