Truly unifying R6RS and R7RS
Daphne Preston-Kendal
(04 Oct 2022 18:22 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(04 Oct 2022 19:16 UTC)
|
||
Re: Truly unifying R6RS and R7RS
John Cowan
(06 Oct 2022 20:30 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(06 Oct 2022 21:11 UTC)
|
||
Re: Truly unifying R6RS and R7RS
John Cowan
(07 Oct 2022 01:33 UTC)
|
||
Re: Truly unifying R6RS and R7RS Marc Nieper-Wißkirchen (07 Oct 2022 08:20 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Arthur A. Gleckler
(07 Oct 2022 18:22 UTC)
|
||
Re: Truly unifying R6RS and R7RS
John Cowan
(07 Oct 2022 22:02 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(08 Oct 2022 10:37 UTC)
|
||
Re: Truly unifying R6RS and R7RS
José Bollo
(27 Oct 2022 07:30 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(27 Oct 2022 08:00 UTC)
|
||
Re: Truly unifying R6RS and R7RS
José Bollo
(01 Nov 2022 14:22 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(01 Nov 2022 14:34 UTC)
|
||
Re: Truly unifying R6RS and R7RS
José Bollo
(03 Nov 2022 08:42 UTC)
|
||
(missing)
|
||
Fwd: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(03 Nov 2022 13:18 UTC)
|
||
Re: Truly unifying R6RS and R7RS
José Bollo
(26 Nov 2022 10:02 UTC)
|
||
Re: Truly unifying R6RS and R7RS
Marc Nieper-Wißkirchen
(26 Nov 2022 17:26 UTC)
|
Am Fr., 7. Okt. 2022 um 03:33 Uhr schrieb John Cowan <xxxxxx@ccil.org>: > > > > On Thu, Oct 6, 2022 at 5:11 PM Marc Nieper-Wißkirchen <xxxxxx@gmail.com> wrote: > >> >> And there seemed to have been some misunderstanding about the why of >> R6RS-style constructors. > > > That's true: the "why" of protocols is not well explained either in R6RS or in TSPLv4. I can give it a try. First of all, a record type should not be mistaken for a class as it is known from object-oriented languages like Java. (I prepend this comment because much of what is said in favor and against class systems and inheritance in typical OOP languages does not apply to Scheme's record-type system.) A record type does not offer the notion of methods, let alone virtual methods (although the record-type facility can be used to implement more OOP-style oriented class facilities, of course). As far as R7RS-small and R6RS without inheritance are concerned, a record is just like a vector whose elements are not accessed by index but by dedicated procedures and whose type is disjoint from the rest of Scheme types. This allows the creation of new opaque Scheme types whose implementation details are encapsulated so that the resulting type (together with the primitive procedures accessing it) can be viewed as the realization of the concept of an abstract data type in a dynamic language like Scheme. The vast majority of uses of the record-type facility of the small language (and of R6RS) are in this spirit. (For evidence for this, one just has to look at the significant number of SRFIs introducing new types into the Scheme language.) Now let us add inheritance. The underlying model is the same in the context of R6RS or any of the various other proposals (SRFI 99, SRFI 131, SRFI 136, SRFI 150), so this simplifies the discussion (and allows me to use the R6RS surface syntax with no loss of generality). Inheritance is just the creation of a new type that is composed of an existing (record) type and a bunch of new fields, where it is arranged so that the accessors and mutators and the predicate of the existing type apply to the composed type as well. In principle, we could therefore go without inheritance by making the composition explicit. So, for example, instead of (define-record-type foo (fields x)) (define-record-type bar (parent foo) (fields y)) we would write (define-record-type (foo make-foo %foo?) (fields (immutable x %foo-x))) (define-record-type bar (fields (immutable foo %bar-foo) y) (protocol (lambda (p) (lambda (x y) (p (make-foo x) y))))) (define (foo? obj) (or (%foo? obj) (bar? obj))) (define (foo-x foo) (if (%foo? foo) (%foo-x foo) (%foo-x (%bar-foo foo)))) The reason why we still want inheritance (in the style of the Scheme record-type facilities) although it is not much more than composition is at least two-fold: The above code with explicit composition adds another indirection as can be seen from the definition of `foo-x`. This inefficiency adds up for each further composition (compared to this, the single-inheritance record-type facility can be implemented so that parent- and child-type fields are arranged linearly in memory). Moreover, the contortions needed in the above code so that the accessors and mutators and the type predicate of the "foo" type apply to the "bar" type do not scale well. The straightforward implementation of composition above (the second code example) has one crucial feature we want to carry over to the composition-through-inheritance model of the Scheme record-type facilities. The "foo" type is an abstract data type (in the sense from the beginning of my explanation) from the viewpoint of the composing data type "bar". To illustrate this, let us look at a similar (but let me skip the contortions mentioned above because they are not relevant here): (define-record-type sq (fields (immutable x sq-ref) (immutable y sq-sqrt-ref)) (protocol (lambda (p) (lambda (x) (p x (sqrt x)))))) The "sq" type models a number, of which I can take a square root. The calculation of the square root is cached, but this is an implementation detail and transparent to the user of this record type (viewed as the reflection of an abstract data type). In the explicit composition model, say when we want to implement a "colored-sq", the composing type needs to access the "sq" type solely through its public interface. Thus the abstraction barrier is not breached. (This is an essential point because the definition and implementation of the "sq" type may be in a different library (or package) so the implementer of the "colored-sq" type would or should not know about the caching of the square root. With custom constructors in the sense of R6RS, this abstraction barrier is preserved. A possible definition of the "colored-sq" type would then be (define-record-type colored-sq (fields color) (parent sq) (protocol (lambda (n) (lambda (x c) ((n x) c))))) As we can see, we do not need to access any implementation details of the "sq" type. Without the protocol device, this is not possible, though, and this is the reason for custom protocols: The definition of the "sq" type itself is not a problem. We can instead write (define-record-type (sq %make-sq sq?) (fields (immutable x sq-ref) (immutable y sq-sqrt-ref))) (define (make-sq x) (%sq x (sqrt x))) The problem arises as soon as we want to inherit from it: (define-record-type (colored-sq %make-colored-sq colored-sq?) (fields color) (parent sq)) (define (make-colored-sq x c) (%make-colored-sq x (sqrt y) c)) Obviously, we need to access what should be implementation details of the "sq" type here. The `make-sq` procedure, which should have provided us with the abstraction cannot be used here. One could try to repair it by having the "sq" package export a procedure like (define (sq-init! sq x) (sq-set! sq x) (sq-square-set! sq (sqrt x))) Moreover, a restricted SRFI 9-style custom constructor would be used for "sq": (define-record-type sq (%make-sq) sq? (x sq-ref) (y sq-square-ref)) This way, the `make-colored-sq` procedure can be defined by (define (make-colored-sq x c) (define rec (%make-colored-sq c)) (sq-init! rec x) x) This, however, is a non-solution for two reasons: First of all, it forces at least the "y" field of the "sq" type to be mutable, which will prevent some possible optimizations (employed by Chez, for example). Moreover, this "solution" still breaks the abstraction barrier around the "sq" package because it is would be possible to construct broken objects of the "sq" type (because the invariant that the "y" field holds the square root of the "x" field is broken). I can think of correct solutions outside of R6RS-syle custom protocols to maintain the abstraction barrier around the "sq" package. but these would be no simpler than the R6RS-solution of custom protocols (instead of exporting the record name "sq" to be used in record-type definitions of child types, the "sq" package would have to define and export a syntax like "define-sq-type" that encapsulates everything related to defining child types). And such a solution would not be integrated into the procedural layer. I hope the above rationale explains the raison d'être of custom protocols for R6RS record types. Unrelated to this, making the definition of custom constructors as simple as possible has another advantage: type checking. So for example, (define-record-type point (fields x y) (protocol (lambda (p) (lambda (x y) (assert (real? x)) (assert (real? y)) (p x y))))) is much better than `(define-record-type point (fields x y))` to define a type of a point in the cartesian plane. With the latter definition, I can create points that are not true points and when I later get errors, e.g. when I attempt to calculate the distance between two points, the location where the error becomes apparent is not where the error originated, making debugging hard. Probably the majority of record types have some constraints for the arguments of their constructors, and it is good when the language suggests checking them at construction time. >> I am not convinced that is necessary to provide the second form of >> `define-record-type` with all bells and whistles as long as R7RS-small >> compatibility is there > > > It is of course not *necessary*, but it accommodates R7RS-small users who wish to make use of one of the R6RS features without learning the entirely new syntax. All right. [...] > Yes. So let's adopt the p-list-style extension to R7RS: (define-record-type (foo parent bar) ...) Yes. >> >> >> But what would it mean to give a protocol (name)? > > > All right, we can do without that (see below). So `parent`, `sealed`, `opaque`, and `nongenerative` would be allowed in any order. See above why this would still make the SRFI 9-style syntax considerably inferior to the R6R-style syntax. >> I am not sure how much this still looks like SRFI 9-syntax when there >> are a lot of entries next to the record name. Would it really be >> better (for SRFI 9-users) than the first syntax given in this SRFI? >> > > Yes, I think so: > > (define-record-type foo > (make-foo bar baz) > foo? > (bar foo-bar-ref) > (baz foo-baz-ref foo-baz-set!)) > > (define-record-type (extfoo parent foo) > (make-extfoo quux) > extfoo? > (quux foo-quux-ref)) > > If this extension to R7RS-small syntax is accepted, the question remaining is whether the purely syntactic features of SRFI-99 should also be provided. I think the answer is no, per SRFI 131. The reason is not, as in SRFI 131, in order to allow a syntax-rules implementation, but simply because SRFI 9 / R7RS-small users are already accustomed to providing explicit names. I think this is a good suggestion. Those who want implicit names (for whatever reasons) can always use the R6RS-style form of the syntax. And those like Daphne who "hate" implicit names get a domain where explicit names are guaranteed. > >> >> In any case, however, the problem of interpreting field names in SRFI >> 9-style constructors in child record-type definitions remains. They >> cannot refer to parent fields (as SRFI 99 says) for R7RS-small >> compatibility, so the only solution seems to be that the names >> correspond solely to child fields and that the parent constructor's >> formal arguments are implicitly added at the beginning. > > > The bulk of all constructors include all the arguments anyway; the typical case is to hide the complete constructor in a library and export an ordinary function that does whatever's necessary to set up the fields correctly. > >> >> (define-record-type colored-point >> (nongenerative) (opaque #t) >> (parent point) >> (fields color) >> (protocol >> (lambda (n) >> (lambda (x y c) >> (assert (memq c '(red green blue)) >> ((n x y) c))))) >> >> What would a translation of this into SRFI 9-style look like? > > > It wouldn't; see above. See above why there should be a way to express it. But I think I have an idea of how to incorporate custom protocols in SRFI 9-style definitions: (define-record-type (sq protocol (lambda (x) (make-sq x (sqrt x)))) (make-sq x y) sq? (x sq-ref) (y sq-sqrt-ref)) (define-record-type (colored-sq parent sq protocol (lambda (x c) (assert (color? c)) ((make-colored-sq x) c)) ((make-colored-sq x) c) colored-sq? (c colored-sq-color)) Here, "x" is a dummy placeholder. The above translates to the R6RS-style definitions: (define-record-type (sq make-sq sq?) (fields (immutable x sq-ref) (immutable y sq-square-ref)) (protocol (lambda (p) (let ((make-sq (lambda (x y) (p x y)))) (lambda (x) (make-sq x (sqrt x))))))) (define-record-type (colored-sq make-colored-sq colored-sq?) (fields (immutable c colored-sq-color)) (protocol (lambda (n) (let ((make-colored-sq (lambda (x) (lambda (c) ((n x) c))) (lambda (x c) (assert (color? c)) ((make-colored-sq x) c)))))) I hope, the translation is obvious enough to understand from the examples. The disadvantage of this proposal is that the constructor subform in the SRFI-9-style syntax is no longer a template for the actually defined constructor.