237: R6RS Records (refined)

by Marc Nieper-Wißkirchen

Status

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.

Table of contents

Abstract

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.

Rationale

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.

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:

Examples

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)))))

Specification

The record mechanism spans four R6RS libraries:

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.

Syntactic layer

The syntactic layer is provided by the (srfi :237 records syntactic) library.

Syntax

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.

Procedural layer

The syntactic layer is provided by the (srfi :237 records procedural) library.

Procedures

(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.

Inspection layer

The syntactic layer is provided by the (srfi :237 records inspection) library.

Procedures

(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.

Reading and writing of record types

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.

Procedures

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.

Implementation

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(…)

Acknowledgements

The foundations of the record facility described here come from R6RS. This SRFI reuses language from R6RS.

  1. 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.


Editor: Arthur A. Gleckler