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:
Construct and return a new record of type foo, providing
space for each field of the record and initializing any fields for which an
initialization is possible and appropriate. The procedure that performs
this operation is called a constructor for the type
foo.
Given any record of type foo, recover from it the value stored
in a particular field. There should be one such procedure for each field
that exists in records of type foo. Such procedures are
called selectors.
Given any record of type foo, store in a particular field of
that record a new value obj. Procedures of this sort are
called mutators. Depending on the particular application, it may
be a good idea to provide a mutator for each field of a record, or for some
and not others, or for none. For example, it might make sense to provide a
mutator for the checked-out? field of the record for a library
book, but one would probably not want to provide a mutator for the
title field.
Determine, for any given Scheme value, whether it is a record of type
foo. This is purpose of the type predicate for the
type foo.
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; the fields in a record are 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 Stella
(make-star "Alpha Centauri 1" '(14 39 26) '(-60 49 25) -0.01 'G2))
> Stella
#(star "Alpha Centauri 1" (14 39 26) (-60 49 25) -0.01 g2)
> (star? Stella)
#t
> (star? (vector 'foo 'bar 'baz))
#f
> (star? 17)
#f
> (star-name Stella)
"Alpha Centauri 1"
> (star-visual-magnitude Stella)
-0.01
> (set-star-right-ascension! Stella '(14 39 26.2))
> Stella
#(star "Alpha Centauri 1" (14 39 26.2) (-60 49 25) -0.01 g2)
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.
Define a procedure filter-by-spectral-type that takes two
arguments -- a list ls of records of type star
and a spectral type -- and returns a list of the names of stars of that
spectral type that are elements of ls.
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). All we need to do
is define an appropriate comparison procedure and specialize our preferred
sorting algorithm to use it:
(define brighter?
(lambda (star-1 star-2)
(< (star-visual-magnitude star-1) (star-visual-magnitude star-2))))
(define sort-by-brightness! (merge-sort! brighter?))
Define a procedure that sorts a vector of records of type
shirt into ascending order by catalog number.
Adapt the binary search procedure so
that it takes two arguments -- a vector vec of records of type
shirt, sorted by the procedure defined in the previous
exercise, and a catalog number catno -- and returns the entire
record that contains that catalog number, if there is one in
vec, or #f if there is no such record.
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/spring-1998/records.html
created April 29, 1997
last revised June 21, 1998