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 specified 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 specified field
of that record a new value obj. Procedures of this sort are
called mutators. Depending on the particular application, it may
or may not be a good idea to provide a mutator for each field; sometimes
one wants all the fields to be mutable, sometimes 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; 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)
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.
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)))
Evaluate the preceding expression to confirm that its value is a datum
that looks just like the definition of square.
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.
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?.
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?.
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.)
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