by Marc Nieper-Wißkirchen
This SRFI is currently in draft 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. It also removes the restriction that the syntactic layer can only define one constructor per record type defined.
Define a datum syntax for records of a nongenerative, non-opaque record type.
Add syntactic sugar for defining constructors.
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, a 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 are 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 three 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.The (srfi :237)
and (srfi :237
records)
libraries are each a composite of these three
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)
, 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
, parent-rtd
identical to the exports by the same name of (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 that 〈record
name〉
is 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 〈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 to
a 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 be a generate 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 is
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.
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.
A portable implementation for R6RS systems with an implementation of SRFI 213 is in this SRFI's repository.
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.