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)

Re: Truly unifying R6RS and R7RS Marc Nieper-Wißkirchen 07 Oct 2022 08:20 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.