Am Mi., 25. Nov. 2020 um 02:52 Uhr schrieb John Cowan <xxxxxx@ccil.org>:


On Tue, Nov 24, 2020 at 1:49 AM Marc Nieper-Wißkirchen <xxxxxx@nieper-wisskirchen.de> wrote:
 
Let us take `string-join` from SRFI 152, for example. It has a GRAMMAR argument, which can be a symbol out of a set of four. If SRFI 152 (or some successor) is designed with SRFI 209 in mind, it makes a lot of sense to turn this symbol argument into an enum argument (which can be dynamically type-checked).  But there are two options: Either the argument becomes a single enum (symbol) or it becomes an enum set (of a single element). The latter would be the preferred form with the recommendation and would allow implementations as fast as if the options were given by raw numbers.

I agree that an enum-type would make sense in this case.  However, I don't see the logic in using an enum-set: eqv? is no slower than fx=?.

It depends on what `string-join` (or some other procedure with enum option arguments) wants to do with the argument.  If all that is needed are tests against a single value each, `eqv?` and `fx=?` would indeed be equally fast.  As soon as a test against a subset is needed, though, `fx=?` will be faster.  As this depends on the implementation of `string-join` or the other procedure, an API that takes a subset looks more future-proof to me.  (This apart from the other advantage that the number of identifiers that has to be exported can be cut down considerably by just exporting the set constructor).
 
 
Moreover, it would allow the compiler to inline code. (The latter would probably be harder if dynamically constructed enum objects would be passed.)

A compiler can do this in cases where define-enum(eration) is in use.
Do you actually want to forbid early error detection or make this optional?

The latter, certainly.

Ah, okay.

 

If early error detection is allowed, one should expect that good implementations detect the error early (even if just for the sake of a superior debugging experience).

By the same token, one should expect that (car 0) generates a compiler error (except on R6RS systems, where it is forbidden to do so), but typically nothing of the sort happens: the program compiles and then at best raises a run-time exception.

Some implementations at least warn when `(car 0)` appears in the code.

Anyway, I think there is quite a difference between syntactic errors and errors that would show up during evaluation (if the relevant code path is taken). 
 
So a coding style by the programmer that enables early error detection (when implemented by the implementation) is very much preferable (and could be hinted at in this SRFI).

I think this is a matter for the implementation to mention: "It is more efficient in this implementation to use define-enum-type instead of make-enum-type where possible."

This should be universally true (to the extent that `define-enum` is not less efficient than `make-enum-type`).  Not only is it hard to imagine an implementation where it wouldn't be true, but such a guarantee is also important in order to write code that is not only portable but that is also efficient on various implementations.

 
If all hings on whether we have procedural macros, I can only repeat my opinion that we should set up the basics first.

The type of procedural macros to be standardized has been and continues to be very controversial.  There are people who detest syntax-case, and others who believe that explicit renaming is an abomination.  This is not a matter that can be adjudicated by reason alone.  R6RS decided on syntax-case in secret and said "Take it or leave it" to the implementer community.  A great many of them left it.  The whole reason *not* to establish this point early is to avoid such an outcome until a great deal of work is already behind us, building trust.

I am committed to a solution publicly arrived at, hopefully by consensus but more probably by majority vote.  A straw vote taken very early in the process selected explicit renaming, but several people did not understand the importance of the question and indicated that they would have voted for syntax-case if they had. 

I understand that there were political reasons why R7RS large took that route, but from a technical perspective, we are building the roof before we have decided on the fundament (modulo some exaggeration).

 
Your Unsyntax system makes it clear that implementing both is perfectly practical, but that doesn't mean it will happen.  In hopes of a resolution, this time I am going to ask the committee to vote on syntax-case and explicit renaming individually.

While I managed to put all of SRFI 211's macro systems in one expander, I don't think that having multiple systems in a standard makes a lot of sense. It would make sense if different problems needed different macro systems to solve. But as far as the existing systems are concerned, it seems there is no sensible macro that you cannot write with syntax-case but with one of the other systems (or a macro where using one of the alternative systems would lead to simpler code).
 
(By the way, I thought that Unsyntax had implicit phasing, but your "Meta definitions" post implies that it doesn't.)

There must be some misunderstanding with my post.  Implicit phasing doesn't mean that there is no phasing.  Only at the REPL, you don't need any concept of phasing (related to the fact that "the top-level is hopeless").  In programs or libraries, the phases at which the binding of an identifier is established have to be sorted out.

Consider the following fragment of a library body:

(define (foo) bar)

(define-syntax quux (lambda (x) ... (foo) ...))

(define (bar) ... quux ...)

If there was no concept of phasing at all, `foo` would be visible at every level (run-time and expand-time) and it would make sense to refer to `foo` in the transformer of `quux`.  But there is a fundamental difference between the expand-time and run-time semantics: Expansion happens in a left-to-right fashion (we have to start somewhere, after all), while the runtime semantics are those of a letrec* construct.  In particular, `foo` is allowed to refer to `bar`.  But `bar` isn't known when `quux` wants to use `foo, so we have to forbid that `quux` uses `foo`.

In order to do so, for each binding, we implicitly (!) record a set of phases at which this binding is valid.  Bindings established through `define` are valid at the phase set {0}.  Bindings established through `define-syntax` are valid at the phase set {0, 1, 2, ...}.

Now, explicit phasing in this context means that the programmer specifies the valid phases of identifiers that are imported by hand.  In an implicit phasing system, all identifiers are imported at all phase levels at once.  As circular library references are not allowed in R[67]RS, the problem sketched above doesn't show up in this case, so we only have to care about the concept of phasing while expanding a single library body.

And here, the `meta` syntax comes in.  It does two things:  First of all, `(meta define (foo) bar)` changes the set of binding phases from {0} to {1, 2, 3, ...}.  Secondly, it disallows forward references (to later meta bindings).  This way, the problem sketched above cannot arise.

Please respond in case something is unclear or you think we talk past each other.