Procedures with keyword arguments should be macros not procedures Marc Nieper-Wißkirchen (17 Oct 2019 09:17 UTC)
Re: Procedures with keyword arguments should be macros not procedures Marc Nieper-Wißkirchen (17 Oct 2019 10:48 UTC)
Intricate lambda list syntax Lassi Kortela (18 Oct 2019 08:26 UTC)
Re: Intricate lambda list syntax Shiro Kawai (18 Oct 2019 09:18 UTC)
Re: Intricate lambda list syntax Lassi Kortela (18 Oct 2019 14:29 UTC)
Re: Intricate lambda list syntax Shiro Kawai (18 Oct 2019 17:32 UTC)

Re: Procedures with keyword arguments should be macros not procedures Marc Nieper-Wißkirchen 17 Oct 2019 10:48 UTC

Thank you very much for your detail reply. I will comment between the
lines below:

Am Do., 17. Okt. 2019 um 12:16 Uhr schrieb Lassi Kortela <xxxxxx@lassi.io>:

> Yes, it traverses a whole plist of keyword arguments on every procedure
> call. I originally had them in a vector, but a vector doesn't work
> portably as a rest argument so I changed it to a list.
>
> Are they noticeably less efficient though? I imagine you'd have to do
> quite a few iterations to have a perceptible speed difference. I would
> not use keyword arguments at all in the fast path; I don't think they
> are the right tool for every job (Common Lisp remarks below).

Sure, it will only make a difference if the procedure in question is
called several times and the work done in the procedure is relatively
small compared to the dispatch overhead.

It won't matter when, say, an HTTP client or server is being
initialized but it will matter a lot if people want to use the
primitives defined by SRFI 177 in cases where speed matters (see also
below). Sometimes, this price in form of slower code has to be paid
(e.g. when you want to use a powerful object system like CLOS), but in
the case of keyword arguments my point is that we do not have to pay
this price thanks to the power of Scheme macros.

> Doubtless true, though the argument representation can be more efficient
> than a list.

It may even be more efficient when it is natively coded in C or
assembler. Yet, it doesn't come for free.

>
> > Therefore, I think it is much better if "procedures" taking keyword
> > arguments are implemented as macros instead, the runtime dispatch
> > overhead into a compile-time dispatch overhead, which is fine.
>
> How would this look in detail? I haven't thought about it very much.

Instead of

(define foo
  (keyword-lambda (a b (c d e))
    (list a b c d e)))

you would write something like (think of a better name than `define-177'...):

(define-177 (foo a b (c d e)) (list a b c d e))

This would bind `foo' to a macro that would be invoked as the
procedure `foo' from the SRFI 177 example above. To avoid infinite
recursion, `foo' may not call itself.

Such a `define-177' cannot be defined through `syntax-rules' though,
because `syntax-rules' do not allow one to compare symbols via
`symbol=?', only `bound-identifier=?' and `free-identifier=?' can be
emulated. In order to make it definable in R5RS and R7RS without
`syntax-case', one would have to use, say, strings instead of symbols:

(define-177 (foo a b ("c" "d" "e")) (list a b c d e))

>
> > Of course, "procedures" taking keyword arguments wouldn't be
> > first-class procedures anymore, but I don't see a convincing use case
> > where this would matter.
>
> It matters in the case where you convert existing non-keyword procedures
> into keyword lambdas, which ought to be quite common. So you start out
> with some procedure like "write-json" and after a few months you realize
> that options are needed for indentation, newlines, character encoding
> and the like. You can then replace
>
> (define (write-json obj port) ...)
>
> with
>
> (define write-json
>    (keyword-lambda (obj port (indent newlines encoding))
>       ...))
>
> The new procedure with keywords can be used the same way as the old one,
> anywhere the old one was used. You don't need to grep the codebase in
> case somebody used it first-class and change those instances.

I think this is exactly what one shouldn't do when the version with
keyword arguments is less efficient than the version without. For
something like `write-json', it is quite conceivable that it is also
used in a fast path.

Nevertheless, there is a workaround. One could extend `define-177'
such that `foo' is not only recognized as a macro keyword but also as
identifier syntax. As identifier syntax, it would expand in a (slow)
procedural version. This would also make it possible to use recursion.
(It would work along the lines `define-integrable' is defined here:
https://cisco.github.io/ChezScheme/csug9.5/syntax.html#./syntax:h1.)

>
> Another case it matters is in anonymous functions. If we had a macro, it
> would have to bind the keyword procedure to a name. The current one is a
> drop-in replacement for lambda.

Where would you want to use anonymous procedures with keyword arguments?

>
> In designing this SRFI my first priority was to have a simple interface
> like this.
>
> > We can even get rid of `keyword-call', whose current need is a bit unfortunate.
> It's a bit verbose, but as you say it's required for portable code if
> procedures are first class. And it makes it clear that keyword magic is
> going on; if keyword lambdas are macros, it's not clear what
> obvious-looking portable syntax we should use to pass the keywords. We
> need to use symbols in some way since we can't rely on portable read
> syntax for keywords.

Instead of

(keyword-call foo 1 2 (d 4 e 5))

you would just write

(foo 1 2 (d 4 e 5)).

> > P.S.: In case someone finds them valuable, a number of arguments in
> > favor and against keyword arguments are raised here:
> > https://github.com/cisco/ChezScheme/issues/108.
>
> Hehe, I was just about to link to that thread in response :D
>
> The efficiency point is valid. However, Common Lisp has had keyword
> arguments for ages and its users rarely complain that they are slow. The
> usual practice is not to have keyword arguments in critical code paths.
> They are best at the public interface of an API that has lots of
> options. A HTTP client is the perfect example: no matter how many
> options you parse, the lion's share of runtime is still taken up by
> network I/O. To me it's like arguing that CLOS classes are slow: they
> are not the right tool for every job.
>
> One lesson from CL is that you do have to do extra work in the compiler
> is fast library functions take keyword arguments. Some of the CL
> string/sequence functions do, and SICL has to compensate (search for
> "define-compiler-macro" in
> <https://raw.githubusercontent.com/robert-strandh/SICL/2cda407575b39b37ec83de988c13c996cba192ba/Code/Sequence/sequences.lisp>).
> Then again those functions are so generic that a fast compiler might
> need to cover those special cases even without keyword arguments,
> leading to about the same amount of code.