Email list hosting service & mailing list manager

Portable keyword arguments Marc Nieper-Wißkirchen (28 Feb 2020 14:44 UTC)
Re: Portable keyword arguments Lassi Kortela (01 Mar 2020 17:35 UTC)
Re: Portable keyword arguments John Cowan (02 Mar 2020 04:38 UTC)
Re: Portable keyword arguments Marc Nieper-Wißkirchen (03 Mar 2020 07:43 UTC)
Re: Portable keyword arguments John Cowan (05 Mar 2020 19:58 UTC)
Re: Portable keyword arguments Marc Nieper-Wißkirchen (06 Mar 2020 07:11 UTC)
Re: Portable keyword arguments Marc Nieper-Wißkirchen (06 Mar 2020 07:45 UTC)

Re: Portable keyword arguments Lassi Kortela 01 Mar 2020 17:35 UTC

Thank you once again for the thoughtful and well expressed comments.

> Here are my current thoughts on keyword arguments in no particular order:
>
> (1)
>
> There seems to be consensus by those working on other SRFIs (for example
> the POSIX ones) that keyword arguments are very much needed for the API.
> (Personally, I would prefer other abstractions as I have discussed with
> John once, but I seem to be in a minority here.) Under this assumption
> it is important that we are able to talk about keyword arguments in
> R7RS-large.

These are like keyword args, but require more design and/or more code:

- record types

- little languages made out of combinators, pass a config object around
and mutate it with procedure sin that language

These are essentially keyword arguments done in a less
standard/efficient/flexible way:

- case-lambda / optional positional arguments (Emacs Lisp offers many
examples that this doesn't scale)

- split a single procedure into multiple procedures, which are variants
of the same behavior with different arguments

- roll your own keyword arguments (alist/plist/hashtable parsing)

(Am I missing any?)

I think the main point here is that keywords args are about convenience
in programming-in-the-large situations where we can't spend a lot of
time to carefully plan every detail. The main disagreement is about
whether the language should exhort people to go ahead and plan anyway
(by using one of the more heavyweight alternatives above). From that
point of view, adding keyword args into the base language would
encourage people to be "sloppy" instead of using record types of little
languages made from reusable combinators.

While I strongly agree that too much focus on convenience is detrimental
to good programming, I think convenience is essential to
programming-in-the-large. Good programmers take pride in well-designed
code, but that can lead to a bit of a taboo against "just solving the
immediate problem and moving on".

In a language core, "just solving the problem" can derail the whole
language and a substantial portion of all the code written in it. But
the further you move away from the core, the less important each bit of
code becomes to a large software system.

If we want Scheme to be used to write large programs, we have to give
programmers some tools to do things that let them solve the immediate
problem without thinking about all the details. Keyword args are nice
because it's immediately obvious how to solve a problem in a way that is
unlikely to turn into a mess later. They have a decades-long track
record where they have proven themselves to be a good tool for that job.
A procedure taking keyword arguments can almost always be salvaged and
recover from design mistake (a badly-designed arg or two) without having
to be thrown away to be replaced by another procedure.

The marathon vs sprint metaphor comes to mind. Big programs are a
marathon, where endurance and covering the distance in the allotted time
are most important. Spending too much effort on individual segments is
actually detrimental to the whole project.

For endurance programming, things like GC, bignums, a library system and
keyword arguments are obvious wins that let people get on with the task
at hand without making an irrecoverable mess.

> For the specification of the consumers of the keyword system, the
> details of the keyword system are, however, not relevant. It is enough
> if the keyword system fulfills a few axioms. In particular, the exact
> syntax of the keyword system is not relevant for the specification of
> the consumers if that specification is written in a way that is
> transparent to the details.

Agreed.

> Likewise, it is probably irrelevant whether
> "procedures" taking keyword arguments are actual procedures or macro
> keywords. Furthermore, whether the keywords are identifiers, objects of
> a keyword type or hygienic identifiers doesn't have to play a role for
> those customers if they are specified with enough care.

I may misunderstand this point, but to me the main point of kw args is
that you don't need to care so much about things ("the luxury of
ignorance"). Hygienic keywords would have to be imported from somewhere;
would it be obvious to callers which modules to import, and how to avoid
keyword clashes?

Shiro and John argued earlier on the SRFI 177 list that the advantage of
non-hygienic keywords is not having to coordinate importing keywords.

> For SRFI 177, this means that it can serve as one model for the keyword
> system that is rich enough for the customers. Example code for the
> customers can be written in terms of SRFI 177. Depending on the
> adaption, how this discussion proceeds, and what syntactic transformers
> will be available in future iterations of R7RS-large, SRFI 177 can then
> easily be replaced by a successor. In particular, the successor may use
> a completely different syntax (so that answers to questions like whether
> it is called lambda/kw or keyword-lambda do not have to be set in
> stone).

Agreed. My feelings are not hurt if it's replaced by something else :)

The only thing I really care about is for every present and future
keyword system to be compatible with every other system. (At least for
the basic usage now covered by SRFI 177; for advanced usage,
compatibility is probably not critical.) Compatibility is a requirement
to make Scheme a contender for many people to write large programs, just
like compatibility of library systems has been a boon to Scheme recently.

> In fact, we can leave SRFI 177 can be underspecified, and this
> may be a good idea. For example, we may leave it open (at least for the
> time being) whether SRFI 177 keyword "procedures" are actual procedures
> or just macros.

Interesting idea. This didn't occur to me; I'll think about it.

My main concern is, will people have to think about difficult things
when changing old non-keyword procedures to keyword ones? Keywords are
about convenience, so having to think about subtle edge cases thoughts
every time one uses them would defeat their purpose.

> (2)
>
> When keywords are hygienic identifiers, "procedures" with keyword
> objects *have* to be special forms. This is probably not too bad. With
> keywords hygienic identifiers, we cannot have a generic `apply' for
> "procedures" taking keyword objects. This is possibly bad.

What causes this - can't a hygienic keyword be a self-evaluating unique
object (similar to a non-interned symbol, which is self-evaluating but
different from every other symbol)? E.g.

(import (rename (foo) (keyword-x keyword-1))
         (rename (bar) (keyword-x keyword-2)))

Would lead to a situation where keyword-1 and keyword-2 could be passed
to apply/kw (keyword-apply in Racket).

(Tangential remark: IMHO keyword-apply is one of the most questionable
things I've encountered in Racket. I had to use it once or twice and
even basic code written using it is almost unreadable. To me, it's a
strong argument that their approach to keywords is too complex.)

> When keywords are non-hygienic objects, keyword procedure calls that
> should in fact be resolved at expand-time have to be resolved at
> run-time and it needs an intelligent compiler to rewrite the call into a
> fast one. This may also relevant for procedures with keyword arguments
> when they are exported from libraries. The Scheme implementation may
> have no way to look into a compiled library, which would make
> cross-library calls of procedures with keyword arguments slow.

Code like this:

(define/kw (foo &key a b c) (list a b c))
(foo :b 2 :a 1)

can be optimized into:

(define (foo/fast-path a b c) (list a b c))
(define/kw (foo &key a b c) (foo/fast-path a b c))
(foo/fast-path 1 2 #f)

Any compiler that can spot :a and :b are self-evaluating objects can
perform the optimization. What am I missing?

Base on my experience with Common Lisp, almost all keyword calls have
the self-evaluating keyword objects at the right places in the argument
list, so almost everything is ripe for optimization in this way.

I guess it can get complex if foo can be redefined into a different
procedure. But it shouldn't be any different from optimizing calls to
particular cases of a case-lambda.

> (3)
>
> There should be a way to write wrappers for "procedures" taking keyword
> arguments. In particular, there has to be a way to pass keyword
> arguments down to callees. With unhygienic keyword arguments, there may
> be the problem that keywords conflict with each other. One way to solve
> it is to make sure that all keywords defined in R7RS-large are disjoint.
> But the way Scheme has solved these problems until now has been hygiene.
> So far, the only things that have to be disjoint are the Scheme library
> names. (The feature identifiers as well but (dummy) library names for
> this purpose would have been sufficient.)

The thing is, conflicting keyword can be either a problem or a solution
depending on what you want. There may be cases where someone's problem
is that keyword that look identical behave differently.

In any case, leaving &allow-other-keys and keyword-apply (or apply/kw)
out of SRFI 177 is almost certainly the right call. There are problems
with how to specify them that are not relevant to the basic everyday
usage of known keyword arguments.

> (4)
>
> If keyword arguments are hygienic, macros can easily generate disjoint
> keyword arguments that do not clash with other keyword arguments.  If
> keyword arguments are symbols or keywords, something like `gensym' is
> needed.  Furthermore, there has to be a way to define procedures that
> take keyword arguments whose names are made on the fly.  The same goes
> for calls with keyword arguments.

Another thing we should think about is the culture that is likely to
develop around keywords. So far, all Lispers and Schemers have used
unhygienic keyword arguments; due to their simplicity, this is what
people are likely to continue using by default. That would lead to a
situation where real-world Scheme code is littered with unhygienic
keywords anyway.

In that case, hygienic keywords would have to be made as easy to use as
the unhygienic keywords that people are used to.

The R7RS library system is an inspiration here. It's quite
sophisticated, yet a Scheme newbie can still easily write a "Hello
world" library without any trouble. Any hygienic keyword system should
either be reserved for advanced usage and tie seamlessly into the
unhygienic keyword system, or be so easy to use that newbies use it
correctly by intuition (just as define-library is easy to use correctly
at the moment).

I agree that making up keyword names on the fly is probably useful from
macros, and should be supported in a compatible way. But IMHO we should
be careful not to end with something keyword-apply in Racket, whose
complexity does not seem to justify the problem it solves. Common Lisp
has a few corners that are just too complex, any way you slice it (loop
and format being the obvious examples). Scheme is so far refreshingly
simple, and that's one of the main parts of its appeal.

In my personal opinion, Racket is a good lesson for Scheme at large. I
find many aspects of it are simply too complex (keyword-apply, pathname
objects, the way immutable and mutable datatypes have incompatible APIs)
and detract from the beauty of Scheme, turning it into another Common
Lisp. But that's a topic for other threads.

> (5)
>
> Any macro that distinguishes the function of an identifier argument by
> its name (for example, whether it begins with ":" or not), is really an
> ugly hack. It may be used for some implementation, but the final
> specification should not depend on this.  It won't work together well
> with the rest of the Scheme system.  I gave one example with `let'.

In practice, we already have the problem that :foo is not portably read
as a symbol. I agree that parsing symbols is a hack, but it merely
levels the playing field so that all implementations then have the same
behavior that some prominent ones already do. With fewer differences
between implementation, the semantics are easier to remember.

I would be fine with having `:foo` read as a keyword object in standard
Scheme like it is in Common Lisp. Then the hack wouldn't be needed.

> Another example is a macro, which expands into a call with keyword
> arguments and that passes down an argument given by the macro user.
> Depending on whether this argument is of the form `:x' or just of the
> form `x' the macro may do completely different things and that would be
> opaque to the macro user.

OK, I see your point. That's true.

However, not all identifiers starting with `:` are parsed - only the
ones that are in "keyword position" in the argument list. Common Lisp
uses keyword args extensively, and it's rare to come across any real
usage that doesn't have the keywords at constant positions.

This problem can be avoided with the (call/kw a b c (d 4 e 5 f 6))
syntax but other people didn't like it :)

Having `call/kw` at the start also says that some magic is going on. If
the ordinary procedure call syntax of Scheme were extended, I would
agree with you that parsing `:` prefixes is too much.

> As long as the Scheme implementations cannot agree on a new fundamental
> type together with its syntax, I would rather add a space in between ":"
> and the identifier and export ":" as auxiliary syntax, e.g. as in
> `(call/kw 1 2 c : 3 d : 4)'.  For this, even `syntax-rules' is
> sufficient.

IMHO the syntax where the colon is separate is hard to read and
needlessly different from other Lisps. You're right that the semantics
are easier. However, most real-world keyword usage in CL, Clojure is
quite pedestrian. A more difficult syntax would make advanced usage
easier but at the cost of detracting from everyday usage.

Maybe hash-table SRFIs are a good point of comparison here. SRFI 69 is
nice and simple; its successors can do more, but most of the time you
only need the basics.

I have no problem with more than one keyword SRFI as long as their
semantics are compatible for the basic use cases covered by 177.

> Furthermore, it would allow a syntax like `(call/kw 1 2 ,c
> : 3 d : 4)', where `c' here is an expression returning a keyword.  This
> is important, see above, to generate new keywords on the fly for example
> in macros.

Good point. This should already possible with the current 177, but you
have to resort to gensym.

; (R7RS records when they are understood with symbols and not
> hygienic identifiers as field names have the serious deficiency that
> hidden fields cannot be added transparently; we don't have to repeat the
> same mistake.)

Good point.

Clashing record field names are one of the best-known problems with
Haskell: <https://wiki.haskell.org/Name_clashes_in_record_fields>.

> (6) The whole discussion show to me that the foundations for R7RS-large
> that is provided by R7RS-small is very beautiful but also quite small.

Indeed. The more I explore it in different contexts, the more I
appreciate what an excellent standard it is. But no standard is the
ideal foundation for every job.

> If we postpone all serious additions to the language that cannot be
> implemented portably and may need some compromises from the R7RS
> implementers to the end of the R7RS-large specification process, the
> majority of the R7RS-large libraries won't use them.

Excellent point. This didn't occur to me. It could be kind of weird to
construct R7RS-large out of parts that use keywords and other parts that
use optional arguments or plists for similar purposes. Would be nice to
try to preserve some unity in standard Scheme, even though in a project
as ambitious as R7RS-large a few compromises have to be made.

> I wonder whether
> it is a good idea to squeeze an R8RS-small or a R7RS-small+ in between
> where the foundations are revised (e.g. by changing the meaning of the
> `:x' syntax).

I would advise against this on PR grounds. With R6RS and R7RS, Scheme is
already a bit confusing to outsiders. If another standard was added (for
any reason - even if it makes sense from a technical point of view) it
will make Scheme's public image more confusing and may decrease morale
for insiders.

What would boost morale the most is to come to an agreement on
fundamental issues like keywords and strings, and ship R7RS-large using
them. John seems to share this opinion, and is doing an excellent job
juggling these conflicting demands.

To the extent that compromises need to be made, I would opt to add
further delay in shipping R7RS-large until we get it right. It will be a
serious blow to Scheme if -large turns out to make unfruitful choices on
core language issues. That's why I very much appreciate that you take
time to argue your points in detail, even against the majority.
Discussions like this are supposed to be difficult even in the ideal
case, and R7RS-large needs to be future-proof on issues like immutable
data and programming-in-the-large, so it's to be expected that it takes
a long time to figure out how it should be.

> (7)
>
> Lassi, do you have a most current version of your SRFI 177 draft? If you
> can give it to me, I will try to make it into an (alternative) proposal
> that addresses some of my points and then we discuss.

I don't have anything more recent than the published first draft. If you
want to draft another document, I can submit a draft #2 that is just
like the current draft but with the names changed to call/kw and
lambda/kw, and adding a define/kw. Those are the only things everyone
has agreed on so far :)

But it seems we have quite a lot of perplexing problems and
disagreements still. It's a long discussion and goes around in circles a
bit, but I don't think that's a problem. There are not that many
difficult issues like this left in the language; if we get keywords,
strings and immutable datatypes sorted, probably the rest of the
language will be easier.