| define-record-type(3scm) | Scheme Programmer's Manual | define-record-type(3scm) |
NAME
define-record-type - define a record typeLIBRARY
(import (rnrs)) ;R6RS (import (rnrs records syntactic)) ;R6RS (import (scheme base)) ;R7RS
SYNOPSIS
; R6RS (define-record-type name-spec record-clause ...) (record-type-descriptor record-name) (record-constructor-descriptor record-name) ; R6RS name-spec (record-name constructor-name predicate-name) record-name ; R6RS record-clause ; The symbols here are exported as auxiliary syntax. (fields field-spec ...) (sealed boolean) (parent parent-name) (parent-rtd parent-rtd parent-rcd) (protocol expression) (opaque boolean) (nongenerative uid) (nongenerative) ; R6RS field-spec (immutable field-name accessor-name) (mutable field-name accessor-name mutator-name) (immutable field-name) (mutable field-name) field-name ; R7RS (define-record-type record-name (constructor-name field-name ...) predicate-name field-spec ...) ; R7RS field-spec (field-name accessor-name) (field-name accessor-name modifier-name)
DESCRIPTION
Defines a record type along with associated constructor descriptor and constructor, predicate, field accessors, and field mutators.- Record names
- The name-spec specifies the names of the record type, constructor, and predicate. In the R6RS variant it can take the form (record-name constructor-name predicate-name) or record-name. In the R7RS variant it can only take the form record-name and the constructor and predicate are specified later.
- The record-name, constructor-name, and predicate-name must all be identifiers. The record-name, taken as a symbol, becomes the name of the record type. See make-record-type-descriptor(3scm).
- The record-name is bound to an expand-time or run-time representation of the record type. In R6RS this binding can be used as parent name in syntactic record-type definitions that extend this definition. It can also be used as a handle to gain access to the underlying record-type descriptor and constructor descriptor by using record-type-descriptor and record-constructor-descriptor.
- The short form of name-spec specifies only record-name and is an abbreviation for the longer form, where the name of the constructor is generated by prefixing the record name with make-, and the predicate name is generated by adding a question mark (?) to the end of the record name.
- For example, if the record name is frob, the name of the constructor is make-frob, and the predicate name is frob?.
- [R6RS] The constructor
- The constructor is bound to constructor-name and is a procedure that constructs instances of the defined record type. It uses a protocol specified by the protocol clause, or, in its absence, using a default protocol. For details on protocols, see the description of the protocol clause below.
- The way the constructor works depends on whether the type is a base type or a subtype, and whether the there is an explicit protocol clause or if the default protocol is used instead. These four cases are described below.
- [R6RS] Constructor for a base type; default protocol
- The constructor accepts as many arguments as there are fields, in the same order as they appear in the fields clause, and returns a record object with the fields initialized to the corresponding arguments.
- [R6RS] Constructor for a subtype; default protocol
- The constructor accepts arguments corresponding to the parent types' constructor first, and then one argument for each field in the same order as in the fields clause, and returns a record object with the fields initialized to the corresponding arguments.
- [R6RS] Constructor for a base type; explicit protocol
- The protocol expression, if it evaluates to a value, must evaluate to a procedure, and this procedure should accept a single argument. The protocol procedure is called once during the evaluation of the define-record-type form with a procedure p as its argument. It should return a procedure, which will become the constructor bound to constructor-name.
- The procedure p accepts as many arguments as there are fields, in the same order as they appear in the fields clause, and returns a record object with the fields initialized to the corresponding arguments.
- The constructor returned by the protocol procedure can accept an arbitrary number of arguments, and should call p once to construct a record object, and return that record object.
- For example, the following protocol expression for a record-type definition with three fields creates a constructor that accepts values for all fields, and initialized them in the reverse order of the arguments:
-
(lambda (p) (lambda (v1 v2 v3) (p v3 v2 v1)))
- [R6RS] Constructor for a subtype; explicit protocol
- The protocol procedure is called once with a procedure n as its argument. As in the previous case, the protocol procedure should return a procedure, which will become the constructor bound to constructor-name. However, n is different from p in the previous case: It accepts arguments corresponding to the arguments of the constructor of the parent type. It then returns a procedure p that accepts as many arguments as there are (additional) fields in this type, in the same order as in the fields clause, and returns a record object with the fields of the parent record types initialized according to their constructors and the arguments to n, and the fields of this record type initialized to its arguments of p.
- The constructor returned by the protocol procedure can accept an arbitrary number of arguments, and should call n once to construct the procedure p, and call p once to create the record object, and finally return that record object.
- For example, the following protocol expression assumes that the constructor of the parent type takes three arguments:
-
(lambda (n) (lambda (v1 v2 v3 x1 x2 x3 x4) (let ((p (n v1 v2 v3))) (p x1 x2 x3 x4)))) - The resulting constructor accepts seven arguments, and initializes the fields of the parent types according to the constructor of the parent type, with v1, v2, and v3 as arguments. It also initializes the fields of this record type to the values of x1, x2, x3 and x4.
- [R6RS] Protocol restrictions and freedoms
- If there is a parent clause, but no protocol clause, then the parent type must not have a protocol clause itself. Similarly, if there is a parent-rtd clause whose parent-rtd evaluates to a record-type descriptor, but no protocol clause, then the parent-rcd expression, if it evaluates to a value, must evaluate to #f.
- A protocol may perform other actions consistent with the requirements described above, including mutation of the new record or other side effects, before returning the record.
- [R7RS] The constructor
- The constructor is bound to constructor-name and it is a procedure that takes as many arguments as there are field-names in the (constructor-name field-name ...) part of the definition and returns a new record of type record-name. Fields whose names are listed with constructor-name have the corresponding argument as their initial value. The initial values of all other fields are unspecified. It is an error for a field-name to appear in constructor but not as a field-name.
- The predicate
- The predicate-name is defined by this definition to a predicate for the defined record type. It is bound to a procedure that returns #t when given a value returned by the procedure bound to constructor-name and #f for everything else. The predicate also returns true for values of subtypes of the record type.
- [R7RS] Record, constructor and predicate names
- These names are always specified explicitly in R7RS. There is no way to get the names automatically generated as in R6RS.
Each record-clause must take one of the forms in the synopsis; it is a syntax violation if multiple record-clauses of the same kind appear in a define-record-type form.
- [R6RS] Field specifications
- Each field-spec has one of the following forms:
- (immutable field-name accessor-name)
(mutable field-name accessor-name mutator-name)
(immutable field-name)
(mutable field-name)
field-name - The field-name, accessor-name, and mutator-name must all be identifiers.
- All forms declare a field called field-name, which is either immutable or mutable.
- The names of the accessor and mutator can optionally be specified manually by supplying accessor-name and/or mutator-name. Otherwise the accessor name is generated by appending the record name and field name with a hyphen separator, and the mutator name (for a mutable field) is generated by adding a -set! suffix to the accessor name.
- For example, if the record name is frob and the field name is widget, the accessor name is frob-widget and the mutator name is frob-widget-set!.
- If field-spec is just a field-name form, it is an abbreviation for (immutable field-name).
- The field-names become, as symbols, the names of the fields in the record-type descriptor being created, in the same order.
- The fields clause may be absent; this is equivalent to an empty fields clause.
- [R7RS] Field specifications
- Each accessor-name is bound to a procedure that takes a record of type record-name and returns the current value of the corresponding field. It is an error to pass an accessor a value which is not a record of the appropriate type.
- Each modifier-name is bound to a procedure that takes a record of type record-name and a value which becomes the new value of the corresponding field; an unspecified value is returned. It is an error to pass a modifier a first argument which is not a record of the appropriate type.
- It is an error for the same identifier to occur more than once as a field name. It is also an error for the same identifier to occur more than once as an accessor or mutator name.
- [R6RS] Sealed record types
- The record clause (sealed #t) defines the record type as sealed, i.e., no extensions of the record type can be created. A sealed record type cannot be used as a parent record type.
- The default, which can be explicitly declared with (sealed #f), is that the defined record type is not sealed.
- [R6RS] Record single inheritance
- The record clause (parent parent-name) specifies that the record type is to have parent type parent-name, where parent-name is the record-name of a record type previously defined using define-record-type.
- The record-type definition associated with parent-name must not be sealed.
- [R6RS] Single inheritance via explicit rtd/rcd
- The clause (parent-rtd parent-rtd parent-rcd) specifies that the record type is to have its parent type specified by parent-rtd, which should be an expression evaluating to a record-type descriptor or #f, and parent-rcd, which should be an expression evaluating to a record-constructor descriptor or #f. See make-record-type-descriptor(3scm) and make-record-constructor-descriptor(3scm).
- If parent-rtd evaluates to #f, then if parent-rcd evaluates to a value, that value must also be #f.
- If parent-rtd evaluates to a record-type descriptor, the record type must not be sealed. Moreover, a record-type definition must not have both a parent and a parent-rtd clause. If no parent clause is present, no parent-rtd clause is present, or a parent-rtd clause is present but parent-rtd evaluates to #f, the record type is a base type.
- [R6RS] Record protocols
- A record protocol can be specified with the (protocol expression) clause. The expression is evaluated in the same environment as the define-record-type form. It must evaluate to a procedure, and this procedure should be a protocol appropriate for the record type being defined.
- The protocol is used to create a record-constructor descriptor as described above ("The constructor"). If no protocol clause is specified, a constructor descriptor is still created using a default protocol. The clause can be absent only if the record type being defined has no parent type, or if the parent definition does not specify a protocol.
- [R6RS] Opaque record types
- The record clause (opaque #t) defines the record type as opaque, which is also the default if the parent record type is opaque. See record-rtd(3scm) for details on what opaque means, but it essentially makes it so that you can't inspect instances of the record type.
- The default, if the record type has no parent or the parent type is not opaque, is that the record type is not opaque. This can be made explicit with (sealed #f).
- [R6RS] Nongenerative record types
- The record type can be made nongenerative with (nongenerative uid) or (nongenerative). The uid must be an identifier. If uid is absent, a unique uid is generated at macro-expansion time.
- If two record-type definitions specify the same uid, then the record-type definitions should be equivalent, i.e., the implied arguments to make-record-type-descriptor(3scm) must be equivalent as described under make-record-type-descriptor(3scm). If this condition is not met, it is either considered a syntax violation or an exception with condition type &assertion(3scm) is raised.
- If the condition is met, a single record type is generated for both definitions.
- In the absence of a nongenerative clause, a new record type is generated every time a define-record-type form is evaluated:
-
(let ((f (lambda (x) (define-record-type r ...) (if x r? (make-r ...))))) ((f #t) (f #f))) => #f
- [R7RS] Record types are always generative
- The define-record-type construct in R7RS is generative: each use creates a new record type that is distinct from all existing types, including Scheme's predefined types and other record types — even record types of the same name or structure.
- [R6RS] Implicit naming
- Any definition that takes advantage of implicit naming for the constructor, predicate, accessor, and mutator names can be rewritten trivially to a definition that specifies all names explicitly. For example, the implicit-naming record definition:
-
(define-record-type frob (fields (mutable widget)) (protocol (lambda (p) (lambda (n) (p (make-widget n)))))) - is equivalent to the following explicit-naming record definition.
-
(define-record-type (frob make-frob frob?) (fields (mutable widget frob-widget frob-widget-set!)) (protocol (lambda (p) (lambda (n) (p (make-widget n)))))) - Also, the implicit-naming record definition:
-
(define-record-type point (fields x y)) - is equivalent to the following explicit-naming record definition:
-
(define-record-type (point make-point point?) (fields (immutable x point-x) (immutable y point-y))) - With implicit naming, it is still possible to specify some of the names explicitly; for example, the following overrides the choice of accessor and mutator names for the widget field.
-
(define-record-type frob (fields (mutable widget getwid setwid!)) (protocol (lambda (p) (lambda (n) (p (make-widget n))))))
- [R6RS] Retrieving the record-type descriptor
- The expression (record-type-descriptor record-name) evaluates to the record-type descriptor associated with the type specified by record-name. See make-record-type-descriptor(3scm).
- The record-type-descriptor procedure works on both opaque and non-opaque record types.
- [R6RS] Retrieving the record-constructor descriptor
- The expression (record-constructor-descriptor record-name) evaluates to the record-constructor descriptor associated with record-name. See make-record-constructor-descriptor(3scm).
- Definition context
- The define-record-type form is a definition and can appear anywhere any other definitions can appear. The define-record-type form expands into a set of definitions in the environment where define-record-type appears; hence, it is possible to refer to the bindings (except for that of the record type itself) recursively.
- Distinct names
- All bindings created by define-record-type (for the record type, the constructor, the predicate, the accessors, and the mutators) must have names that are pairwise distinct.
IMPLEMENTATION NOTES
Both the syntactic and procedural definitions of record can be optimized using techniques from the paper A Sufficiently Smart Compiler for Procedural Records by Andy Keep and R. Kent Dybvig.RETURN VALUES
This syntax can not be used in a value context.EXAMPLES
;; This is an example from R7RS
; The following record-type definition
(define-record-type <pare>
(kons x y)
pare?
(x kar set-kar!)
(y kdr))
; defines kons to be a constructor, kar and kdr to be ac-
; cessors, set-kar! to be a modifier, and pare? to be a
; predicate for instances of <pare>.
(pare? (kons 1 2)) => #t
(pare? (cons 1 2)) => #f
(kar (kons 1 2)) => 1
(kdr (kons 1 2)) => 2
(let ((k (kons 1 2)))
(set-kar! k 3)
(kar k)) => 3
; This is an example from R6RS
(define-record-type (point make-point point?)
(fields (immutable x point-x)
(mutable y point-y set-point-y!))
(nongenerative
point-4893d957-e00b-11d9-817f-00111175eb9e))
(define-record-type (cpoint make-cpoint cpoint?)
(parent point)
(protocol
(lambda (n)
(lambda (x y c)
((n x y) (color->rgb c)))))
(fields
(mutable rgb cpoint-rgb cpoint-rgb-set!)))
(define (color->rgb c)
(cons 'rgb c))
(define p1 (make-point 1 2))
(define p2 (make-cpoint 3 4 'red))
(point? p1) => #t
(point? p2) => #t
(point? (vector)) => #f
(point? (cons 'a 'b)) => #f
(cpoint? p1) => #f
(cpoint? p2) => #t
(point-x p1) => 1
(point-y p1) => 2
(point-x p2) => 3
(point-y p2) => 4
(cpoint-rgb p2) => (rgb . red)
(set-point-y! p1 17) => unspecified
(point-y p1) => 17)
(record-rtd p1)
=> (record-type-descriptor point)
(define-record-type (ex1 make-ex1 ex1?)
(protocol (lambda (p) (lambda a (p a))))
(fields (immutable f ex1-f)))
(define ex1-i1 (make-ex1 1 2 3))
(ex1-f ex1-i1) => (1 2 3)
(define-record-type (ex2 make-ex2 ex2?)
(protocol
(lambda (p) (lambda (a . b) (p a b))))
(fields (immutable a ex2-a)
(immutable b ex2-b)))
(define ex2-i1 (make-ex2 1 2 3))
(ex2-a ex2-i1) => 1
(ex2-b ex2-i1) => (2 3)
(define-record-type (unit-vector
make-unit-vector
unit-vector?)
(protocol
(lambda (p)
(lambda (x y z)
(let ((length
(sqrt (+ (* x x)
(* y y)
(* z z)))))
(p (/ x length)
(/ y length)
(/ z length))))))
(fields (immutable x unit-vector-x)
(immutable y unit-vector-y)
(immutable z unit-vector-z)))
(define *ex3-instance* #f)
(define-record-type ex3
(parent cpoint)
(protocol
(lambda (n)
(lambda (x y t)
(let ((r ((n x y 'red) t)))
(set! *ex3-instance* r)
r))))
(fields
(mutable thickness))
(sealed #t) (opaque #t))
(define ex3-i1 (make-ex3 1 2 17))
(ex3? ex3-i1) => #t
(cpoint-rgb ex3-i1) => (rgb . red)
(ex3-thickness ex3-i1) => 17
(ex3-thickness-set! ex3-i1 18)
=> unspecified
(ex3-thickness ex3-i1) => 18
*ex3-instance* => ex3-i1
(record? ex3-i1) => #f
APPLICATION USAGE
Record types are used extensively to create new data types. Older Lisp and Scheme code often used pairs and vectors, but this resulted in messy code that was hard to follow.COMPATIBILITY
The R7RS variant of define-record-type is based on SRFI-9.The R6RS variant is unique to R6RS. Porting it to a Scheme with only syntax-rules(3scm) is not possible because that macro language can not create names that were not present in the original source code.
ERRORS
This syntax can raise exceptions with the following condition types:- &syntax (R6RS)
- There was a syntax violation.
- R7RS
- There are several situations in this manual page that are described as errors. Implementations may signal an error, extend a procedure's domain of definition to include such arguments, or fail catastrophically.
SEE ALSO
make-record-type-descriptor(3scm)STANDARDS
SRFI-9, R6RS, R7RSAUTHORS
This page is part of the scheme-manpages project. It includes materials from the RnRS documents. More information can be found at https://weinholt.se/scheme/manpages/.| 2023-08-08 |