by Marc Nieper-Wißkirchen
This SRFI is currently in final status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-237@nospamsrfi.schemers.org
. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.
The record mechanism of R6RS is refined. In particular, the triad of record names, record-type descriptors and record constructor descriptors can be effectively ignored and replaced with the single notion of a record descriptor. We also remove the restriction that the syntactic layer can only define one constructor per record type defined.
Objections against the R6RS record system were were voiced by people voting against ratification of that standard's latest candidate draft. These objections touched the complexity, the role of the procedural layer, and the compatibility between the syntactic and the procedural layer of the R6RS record system. This SRFI addresses these objections while remaining compatible with the R6RS record system.
The conceptual distinction between record
names, record-type descriptors, and record constructor
descriptors made in R6RS has been perceived as
challenging by users. This SRFI therefore refines the
R6RS record mechanism so that the triad of record
names, record-type descriptors and record constructor
descriptors can be effectively ignored and replaced with the
single notion of a record descriptor. In particular,
the parent-rtd
clause and
the record-type-descriptor
and
the record-constructor-descriptor
syntax are no
longer needed.
The procedural layer is intended to be used when record types have to be constructed at runtime, e.g. by interpreters that need to construct host-compatible record types. The procedural layer, much like the inspection layer, is therefore not needed for most code. Therefore, the vast majority of programmers can ignore the procedural layer. This is strengthened by this SRFI, which removes the restriction that the syntactic layer can only define one constructor per record type defined.
The syntactic and the procedural layer are compatible, and a syntactically defined record type can inherit from a procedurally defined record type, and vice versa. Whether a record type has been defined syntactically or procedurally is generally unobservable. To inherit from a record type, all that has to be exposed from it is either the (bound) record name or the record type descriptor (with or without a record constructor descriptor). The latter is simplified by this SRFI, as the latter three entities can be used effectively interchangeably.
Record type definitions and the record types defined through them can have one or more of the following attributes in R6RS: non-generativity, sealedness, and opacity. Each has its respective raison d'être:
A record type (definition) is non-generative if evaluating a definition creates a new record type only once per expansion of the defining form. This makes local record type definitions to take advantage of lexical scoping feasible. In fact, non-generative record type definitions behave as definitions of records/structures/classes in statically typed languages.
If a record type is sealed, no extensions of the
record type can be created. While this allows a compiler to
generate slightly more efficient code for such a record
type's accessors and mutators, the real use of sealedness is
that it can guarantee correctness. When the type predicate
of a Scheme record type returns #t
for a record
type, it also returns #t
on all child record
types of this type. Thus, the argument types of procedures
that have to rely on the type predicate to check for type
correctness are automatically contravariant. Sealedness
breaks contravariance and is thus a method to define
procedures for record types that are non-contravariant.
An opaque record type cannot be inspected through the inspection layer. Unless its record name or record type-descriptor is exported, it behaves as a non-record type. Opaque record types can thus be used to implement fundamental types like pairs while enforcing portability of code. Most of the disjoint types introduced by SRFIs should be implemented as opaque record types.
Record types defined by the syntactic and by the procedural layer are fully compatible, and can simply inherit from each other:
(define-record-type rec1 (fields a) (protocol (lambda (p) (lambda (a/2) (p (* 2 a/2)))))) (define rec2 (make-record-descriptor 'rec2 rec1 #f #f #f '#((immutable b)) (lambda (n) (lambda (a/2 b) ((n a/2) b))))) (define make-rec2 (record-constructor rec2)) (define rec2? (record-predicate rec2)) (define rec2-b (record-accessor rec2 0)) (define-record-type rec3 (parent rec2) (fields c) (protocol (lambda (n) (lambda (c) ((n c c) c)))))
The following library defines a simplified abstract (read-only) dictionary type, which can appear as a dictionary based on a hash table or on an alist. For this, it defines one record type, but with several names:
(library (example dictionary) (export dictionary dictionary? dictionary-ref dictionary-from-hashtable make-dictionary-from-hashtable dictionary-from-alist make-dictionary-from-alist) (import (rnrs base (6)) (rnrs hashtables (6)) (srfi :237)) (define-record-type dictionary (nongenerative) (opaque #t) (fields ht) (protocol (lambda (p) (lambda args (assert #f))))) (define dictionary-ref (lambda (dict key default) (assert (dictionary? key)) (hashtable-ref (dictionary-ht dict) key default))) (define-record-name (dictionary-from-hashtable dictionary) (protocol (lambda (p) (lambda (ht) (assert (hashtable? ht)) (p ht))))) (define-record-name (dictionary-from-alist dictionary) (protocol (lambda (p) (lambda (alist) (define ht (make-eqv-hashtable)) (assert (list? alist)) (for-each (lambda (entry) (assert (pair? entry)) (hashtable-set! ht (car entry) (cdr entry))) alist) (p ht))))))
This library can be used to define child record types based on either appearance:
(define-record-type owned-dictionary (parent dictionary) (fields owner) (protocol (lambda (n) (lambda args (assert #f))))) (define-record-name (owned-dictionary-from-hashtable owned-dictionary) (parent dictionary-from-hashtable) (protocol (lambda (n) (lambda (ht owner) ((n ht) owner))))) (define-record-name (owned-dictionary-from-alist owned-dictionary) (parent dictionary-from-alist) (protocol (lambda (n) (lambda (alist owner) ((n alist) owner)))))
The record mechanism spans four R6RS libraries:
(srfi :237 records syntactic)
library, a
syntactic layer for defining a record type and associated
constructor, predicate, accessor, and mutators,(srfi :237 records procedural)
library, a
procedural layer for creating and manipulating record types and
creating constructors, predicates, accessors, and mutators,(srfi :237 records inspection)
library, a
set of inspection procedures.(srfi :237 records ports)
library, a set of
procedures for controlling external representations of
records.The (srfi :237)
and (srfi :237
records)
libraries are each a composite of these four
libraries. This composite library exports all procedures and
syntactic forms provided by the component libraries.
The corresponding R7RS library names are (srfi
237 syntactic)
,
(srfi 237 procedural)
, (srfi 237
inspection)
, (srfi 237 port)
,
and (srfi 237)
, respectively.
The record mechanism described in this SRFI is based on the record mechanism described in R6RS. Unless said otherwise, definitions and semantics remain unchanged from R6RS.
A record descriptor is what is called a record constructor descriptor in R6RS. The term record constructor descriptor is deprecated.
The naming convention rd
in the
procedure entries below implies that the type of the argument must
be a record descriptor.
The type of record descriptors is a subtype of the type of
record-type descriptors. A record-type descriptor that is not a
record descriptor is a simple record-type descriptor.
Each record descriptor has an underlying simple record-type
descriptor and an underlying parent descriptor.
The underlying simple record-type descriptor of a simple
record-type descriptor is the simple record-type descriptor
itself. The underlying parent descriptor of a record descriptor
representing a base record type is #f
.
Whenever a syntax or procedure described below expects a record-type descriptor, the result is equivalent to when the record-type descriptor is replaced by its underlying simple record-type descriptor.
Note: Conceptually, a record descriptor is a simple record-type descriptor together with an R6RS record constructor descriptor whose associated record type is represented by the simple record-type descriptor.
Note: Most users can safely ignore the notion of an underlying simple record-type descriptor and just use record descriptors.
The syntactic layer is provided by the (srfi :237 records
syntactic)
library.
The library exports the auxiliary
syntax fields
, mutable
, immutable
, parent
, protocol
, sealed
, opaque
, nongenerative
, and parent-rtd
,
each identical to the export by the same name from (rnrs records
syntactic (6))
. It also exports the auxiliary
syntax generative
.
(define-record-type 〈name spec〉 〈record clause〉 …)
Syntax: This syntax is equivalent to the syntax of the
record-type-defining form define-record-type
exported by (rnrs records syntactic (6))
.
Semantics:
This definition is equivalent to the record-type-defining
form define-record-type
exported by (rnrs
records syntactic (6))
with the followings additions:
The 〈record name〉
is bound to a record
name, which is a keyword. As an expression, this keyword
evaluates to the underlying record descriptor whose underlying
simple record-type descriptor is the simple record-type
descriptor associated with the type specified by 〈record
name〉
and whose underlying parent descriptor is the record descriptor of the
parent of the type, or #f
in case of a base type.
Note: R6RS allows 〈record
name〉
to be bound to an expand-time or run-time representation.
The 〈name spec〉
can also be of the
form (〈rtd name〉 〈record name〉
〈constructor name〉 〈predicate
name〉)
or (〈rtd name〉 〈record
name〉)
. In these cases, 〈rtd
name〉
, instead of 〈record
name〉
, taken as a symbol, becomes the name of the
record type. The second form is an abbreviation for the first
form where the constructor and the predicate name is derived
from the 〈rtd name〉
, instead of
the 〈record name〉
.
In a parent
clause, the 〈parent
name〉
can be either a record name or an expression
that must evaluate to a record-type descriptor or record
descriptor. If 〈parent name〉
is a record
name, the parent
clause is equivalent to
a parent
clause of the R6RS syntactic
layer.
Otherwise, if the expression evaluates to a record-type
descriptor rtd
, the parent
clause is equivalent to the
clause (parent-rtd rtd #f)
of the
R6RS syntactic layer.
Finally, if the expression evaluates to a record
descriptor rd
, the parent
clause is equivalent to the
clause (parent-rtd rd rd)
of the
R6RS syntactic layer.
The parent-rtd
clause is deprecated.
A clause of the (generative)
specifies that the
record type is generative. This clause is mutually exclusive with
the non-generative
clause.
Note: While this clause is superfluous as its absence
(and the absence of a non-generative
clause) also
specifies that the record type is generative, it makes it
sensible to encourage implementations to raise a continuable
exception of type &warning
when
a define-record-type
with neither
a generative
nor non-generative
clause
is expanded. This helps detecting accidental specifications of generative record types.
Note: An implementation is also encouraged to raise a
continuable exception of type &warning
when
a non-generative
clause without
a 〈uid〉
is specified, as such record types
are not compatible across different expansions of
a define-record-type
form, e.g. when a library is
visited more than once.
Remark: In Chez Scheme, the equivalent of the
clause (generative)
is a clause of the
form (nongenerative #f)
. This syntax is not used
in this SRFI to avoid double-negation and misunderstanding
because #f
often stands for a default value, which,
in this context, could have been a generated uid.
(define-record-name 〈name spec〉 〈record clause〉 …)
Syntax: The 〈name spec〉
is of the
form (〈record name〉 〈record type〉
〈constructor name〉)
or (〈record
name〉 〈record type〉)
.
The 〈record name〉
must be an identifier,
the 〈record type〉
a record name or an
expression.
The second form of 〈name spec〉
is an
abbreviation for the first form, where the name of the
constructor is generated by prefixing the record name
with make-
.
A 〈record clause〉
is as for
the define-record-type
syntax except that only
a parent
and a protocol
clause are
allowed.
Semantics: If 〈record type〉
is
bound to a record name (e.g. by a
previous define-record-type
or define-record-name
definition),
let rd be the underlying record descriptor.
Otherwise, 〈record type〉
must evaluate to
a record descriptor rd.
This definition binds the 〈record name〉
to a record name as defined in the description of
the define-record-type
definition. The underlying
record descriptor is one whose underlying simple record-type
descriptor is the simple record-type descriptor underlying rd and
whose underlying parent descriptor is the record descriptor of
the parent, or the underlying parent descriptor
of rd.
The protocol
clause specifies the constructor
descriptor of the record descriptor
underlying 〈record name〉
.
(record-type-descriptor 〈record name〉)
Equivalent to the syntax with the same name in R6RS.
The record-type-descriptor
syntax is deprecated.
Note: Use (record-descriptor-rtd 〈record name〉)
instead.
(record-constructor-descriptor 〈record name〉)
Equivalent to the syntax with the same name in R6RS.
The record-constructor-descriptor
syntax is deprecated.
Note: Use (values 〈record name〉)
instead.
The syntactic layer is provided by the (srfi :237 records
procedural)
library.
(make-record-type-descriptor name parent uid sealed? opaque? fields)
Equivalent to the procedure with the same name in R6RS. The returned record-type descriptor is a simple record-type descriptor.
(record-type-descriptor? obj)
Equivalent to the procedure with the same name in R6RS.
(make-record-descriptor rtd parent-descriptor protocol)
(make-record-constructor-descriptor rtd parent-descriptor protocol)
Equivalent to the procedure with the latter name in
R6RS. The underlying simple record-type descriptor
of the returned record descriptor is the underlying simple
record-type descriptor of rtd
. The
underlying parent descriptor of the record descriptor is
the parent-descriptor
.
The name make-record-constructor-descriptor
is
deprecated.
(make-record-descriptor name parent uid sealed? opaque? fields
protocol)
Equivalent to (make-record-descriptor (make-record-type-descriptor name parent uid sealed? opaque? fields)
parent protocol)
.
(record-descriptor-rtd rd)
Returns the underlying simple record-type descriptor of rd
.
(record-descriptor-parent rd)
Returns the underlying parent record descriptor of rd
.
(record-descriptor? obj)
(record-constructor-descriptor? obj)
Returns #t
if the argument is a record
descriptor, #f
otherwise.
Note: This predicate is missing in R6RS. According to at least one of the editors, it shouldn't be.
The name record-constructor-descriptor?
is
deprecated.
(record-constructor rd)
Equivalent to the procedure with the same name in R6RS.
(record-predicate rtd k)
Equivalent to the procedure with the same name in R6RS.
(record-accessor rtd k)
Equivalent to the procedure with the same name in R6RS.
(record-mutator rtd k)
Equivalent to the procedure with the same name in R6RS.
The syntactic layer is provided by the (srfi :237 records
inspection)
library.
(record? obj)
Equivalent to the procedure with the same name in R6RS.
(record-rtd record)
Equivalent to the procedure with the same name in R6RS.
(record-type-name rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-parent rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-uid rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-generative? rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-sealed? rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-opaque? rtd)
Equivalent to the procedure with the same name in R6RS.
(record-type-field-names rtd)
Equivalent to the procedure with the same name in R6RS.
(record-field-mutable? rtd k)
Equivalent to the procedure with the same name in R6RS.
(record-uid->rtd uid)
The uid
must be a symbol.
If a previous call to make-record-type-descriptor
was made with the uid
symbol, return the
same (in the sense of eqv?
) record-type descriptor
as this previous call. Otherwise, return a record-type
descriptor rtd
such
that (record-type-uid rtd)
would return
the symbol uid
, or #f
.
Records of non-generative, non-opaque record types whose field values are datum values are added to the set of Scheme datum values and thus have an external representation. These records are constant literals; an expression consisting of a representation of such a record therefore evaluates "to itself". (Note that literal constants are immutable objects; this is regardless of whether a record type has mutable fields.)
The external representation for a record is given
by #r(〈rtd〉 〈datum〉
…)
where 〈rtd〉
is a
representation (see below) of the record type's record-type
descriptor and the 〈datums〉
are external
representations of the record's field values.
An external representation 〈rtd〉
of a
record-type descriptor (of a non-generative record-type) is
either an external representation of its uid or a list-like
representation given by (〈name datum〉
〈parent rtd〉 〈uid datum〉 〈sealed?
datum〉 〈opaque? datum〉 〈fields
datum〉)
where the 〈name
datum〉
, 〈uid datum〉
,
〈sealed? datum〉
, 〈opaque?
datum〉
, and 〈fields datum〉
correspond to external representations of the respective
arguments to make-record-type-descriptor
and 〈parent rtd〉
is an external representation
of the parent's record-type descriptor as defined in this
paragraph, or #f
if the record-type is a base
type.
External representations of record-type descriptors are not used outside of external representations of records.
The ancestor types of a record-type of a read or written record should be non-generative as well.
Shared or cyclic structure may be created as long as all cycles are resolvable, however, without mutation of an immutable record field.
The lexical syntax #!srfi-237
indicates that
subsequent input may contain external representations of records
as defined here. Otherwise, it is treated as a comment.
Implementations of SRFI 237 must not
treat #!srfi-237
as a lexical violation.
Remark: The syntax #!srfi-237
is useful for
signifying implementations that cannot support the external
representations of records as defined here in their default or
native modes.
Note: Syntactic datums of records may appear in
quotations, in syntax objects, in syntax-case
patterns, and in syntax
templates. The expander
does not push mark and substitution wraps down into record
datums.
The following are examples of external representations of records assuming the following definitions.
(define-record-type point (nongenerative point-6366d320-a1dd-48f9-b13f-5543399c1a90) (fields x y)) (define-record-type colored-point (nongenerative colored-point-e6abbd89-f453-4354-985e-12f17fbf35c2) (parent point) (fields color)) #r(point-6366d320-a1dd-48f9-b13f-5543399c1a90 1.0 2.0) #r(colored-point-e6abbd89-f453-4354-985e-12f17fbf35c2 1.0 2.0 'red) #r((point #f point-6366d320-a1dd-48f9-b13f-5543399c1a90 #f #f #((mutable x) (mutable y))) 1.0 2.0) #r((colored-point (point #f point-6366d320-a1dd-48f9-b13f-5543399c1a90 #f #f #((mutable x) (mutable y))) colored-point-e6abbd89-f453-4354-985e-12f17fbf35c2 #f #f #((mutable color))) 1.0 2.0 'red) #r((colored-point point-6366d320-a1dd-48f9-b13f-5543399c1a90 colored-point-e6abbd89-f453-4354-985e-12f17fbf35c2 #f #f #((mutable color))) 1.0 2.0 'red)
The third datum syntax represents the same record as the first, and the fourth and fifth datum syntax the same record as the second. The third and fourth syntaxes are self-contained (and don't need the preceding explicit definitions). The fifth is self-contained once the third is read.
The following procedures are provided by the (srfi :237 records
ports)
library.
Each textual input port has an associated parameter
(see SRFI
226), called the rtd read flag, whose initial
value is #t
.
Each textual output port has an associated parameter, called
the rtd write flag, whose initial value
is #f
.
When the value of the rtd read flag is #f
, the
external representation of a record-type descriptor must be the
external representation of a uid symbol of a previously created
record-type when syntactic data is read from the port.
Otherwise, when the value of the rtd read flag is
not #f
, and the external representation of a
record-type descriptor is not the external representation of a
uid symbol of a previously created record-type, it must be a
list-like external representation of the record-type descriptor
and the corresponding record-type descriptors are created (in
left-to-right order) as if through
calling make-record-type-descriptor
.
Remark: When reading from an untrusted source, the rtd
read flag should be set to #f
as non-generative
record-type definitions cannot be garbage-collected.
When the value of the rtd write flag is #f
,
external representations of a record-type descriptor are just
the external representations of a uid symbol when syntactic data
is written to the port. Otherwise, at least as many list-like
external representations of record-type descriptors are included
in the external representations of records when a syntactic
datum is written to the port such that it can be read back from
the port without prior calls
to make-record-type-descriptor
.
(port-read-rtd textual-input-port)
Returns the rtd read flag of
textual-input-port
.
(port-write-rtd textual-output-port)
Returns the rtd write flag of
textual-output-port
.
A portable implementation for R6RS systems with an implementation of SRFI 213 is in this SRFI's repository.
The sample implementation does not support reading and writing
of record types as this would have implied a rewrite
of get-datum
and put-datum
.
Chez Scheme supports some of the external representations,
albeit using the lexical syntax #[…]
instead
of #r(…)
The foundations of the record facility described here come from R6RS. This SRFI reuses language from R6RS.
Michael Sperber, R. Kent Dybvig, Matthew Flatt, Anton van Straaten, Robby Findler, and Jacob Matthews: Revised6 Report on the Algorithmic Language Scheme. Journal of Functional Programming, Volume 19, Supplement S1, August 2009, pp. 1-301. DOI: 10.1017/S0956796809990074.
© 2022 Marc Nieper-Wißkirchen.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.