Re: Procedures with keyword arguments should be macros not procedures Lassi Kortela 17 Oct 2019 10:16 UTC
> SRFI 177 gives a number of good reasons why one wants to add/invent > keyword arguments to procedures. One reason is those keyword arguments > allow one to introduce new parameters without breaking old code. > > However, this shouldn't come with the price of decreased efficiency. > > In the portable R5RS/R7RS sample implementation coming with SRFI 177, > whenever a procedure defined with `keyword-lambda' is being called, a > lot of work for the keyword dispatch is done, which causes procedures > defined through `keyword-lambda' to be noticeably less efficient than > ordinary procedures defined through `lambda'. 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). Apart from efficiency concers, classic Common Lisp-style keyword args are also hard to type-check statically. IIRC that's why Kawa and Racket have new keyword semantics where a keyword is just a syntactic marker in a procedure call, and not a self-evaluating object. > The same will most likely be true for native implementations when the > call site does not know the called procedure as it will most likely > have to resort to runtime dispatching as well. Doubtless true, though the argument representation can be more efficient than a list. > 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. > 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. 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. 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. > 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.