Re: Wrapping up SRFI 177: Portable keyword arguments Lassi Kortela 22 Feb 2020 22:17 UTC
> However, I'm wondering what good lambda/kw actually is, since there is > not and cannot be apply/kw, and plain apply forces all the keyword > values to the default. call/kw is apply/kw: (define (just-for-fun thunk) (call/kw thunk 1 2 :c 3 :d 4)) (just-for-fun (lambda/kw (a b &key c d) (list a b c d))) ; => (1 2 3 4) I don't know whether this is useful. I just did it because it's the least deviation from ordinary lambda. Another thing is that passing around a keyword lambda for use as a higher-order function is nice, even if it gets default values for all its keyword arguments that way. It's still possible to call it with keyword arguments from other contexts. This arrangement makes it possible to add keyword arguments to existing procedures that are already being used as higher-order functions. This is a good example of the kind of situation where I want people to be able to add keyword arguments without a lengthy and difficult analysis that "will this break some existing code"? > It sort of reminds me of not having > generic-function-lambda in CL; what's the point of an anonymous generic > function? An anonymous generic function would be like case-lambda, but casing on argument types instead of the length of the lambda list. > Common Lisp compilers can optimize keyword calls, > > But probably not when the procedure-with-keywords is a lambda. In principle, it should have more to do with whether the particular lambda being called is known at the call site or not. If it's known, it can be inlined like any other lambda. > &key is standard in most Lisps and does not step on native keywords. Stepping on native keywords in the lambda list shouldn't be a problem. However, favoring &key because it's more standard is fine. DSSSL and SRFI 89 use #!key, but if you want SRFI 177 in R7RS-large after all, using something other than #!key will make it possible to avoid mandating extra syntax in the language. > ObDigression: ISLisp does not require runtime keyword objects to exist, > because they are used only as syntactic keywords in various special > forms/macros (no explicit distinction in the spec) and there is a fixed > list, namely :abstractp, :accessor, :after, :around, :before, :boundp, > :generic, :initarg, :initform, :metaclass, :method, :reader, :rest, > :writer. However, &rest can be used instead of :rest. Interesting to know. Sounds similar to Kawa and Racket. > Anyone who uses both keyword arguments and normal identifiers that look > like keywords deserves to lose. At least the portable use of keywords > is confined to these two macros. I agree that it's unwise. I think Marc's point was more about permitting a simple language definition without hacks than about what people should use in practice. > Getting Scheme implementers to accept new lexical syntax is one of the > most difficult things to arrange for, because it often requires pulling > apart hand-coded parsers. CL-style readtables make it straightforward, > but only Racket, Gambit, Chicken, and Guile (through Guile-Reader) have > them. So I am very much against this. Indeed, it is why I have > postponed all lexical proposals to the very end of R7RS-large work, and > treat any lexical syntax in existing SRFIs as merely advisory until then. Sorry, I must have missed the memo. In that case you can't have keyword arguments in R7RS in such a way that they easily permit supporting hygienic keywords with the same syntax as non-hygienic ones. All the non-hideous syntaxes for mixing hygienic and non-hygienic keywords rely on using keyword objects for non-hygienic keywords, and treating ordinary identifiers in "keyword position" as hygienic keywords. If there are no keyword objects in the language, then ordinary identifiers in "keyword position" (or ordinary identifiers whose names at make them look like keywords macro-expansion time) have to stand for non-hygienic keywords. > IMO the whole point of keywords is that they are not hygienic: they have > absolute meaning. I'm open to being convinced otherwise, of course, but > nobody has done even that, still less implemented them. I think there's some merit to the hygienic keywords approach, but it's quite experimental whereas non-hygienic keywords have been common for decades. In practice, having to explicitly export and import keywords would probably cause confusion to most people and would obviate much of the benefit of keyword args (which is convenience - they are a more convenient way to do the same thing you could do by defining a record type). Since keywords are all about programming-in-the-large, working well with other people's unfamiliar code is all-important. A non-hygienic keyword "does what it says on the tin" so it's good from that point of view as well. If a hygienic keyword is imported, what if you accidentally imported it from the wrong library, or need to import conflicting keywords with the same name from two different libraries? > * Since SRFI 177's main goal is compatibility with existing Scheme > keyword systems, it will use the syntax (call/kw foo 1 2 :d 3 :e 4). > Here, :d and :e can be either identifiers (in any Scheme implementation) > > Only if it does not automatically treat :d as not an identifier. The call/kw macro needs to have a check like: (define (parse-keyword elem) (if (keyword? elem) elem (and (symbol? elem) (let* ((s (symbol->string elem)) (n (string-length s))) (and (not (= 0 n)) (cond ((char=? #\: (string-ref s 0)) (string->keyword (substring s 1 n))) ((char=? #\: (string-ref s (- n 1))) (string->keyword (substring s 0 (- n 1)))) (else #f))))))) (parse-keyword 'foo) => #f (parse-keyword ':foo) => foo: (parse-keyword 'foo:) => foo: (parse-keyword foo:) => foo: That will make it work in all implementations. > call/kw would scan for those literal symbols at read time, > > At macro expansion time, presumably. Yes, that's better terminology. call/kw would not eval anything to decide at runtime what is a keyword and what is not. > On the contrary, I believe that SRFI 177 is the *only* hope of getting > keyword arguments into R7RS, politically and logistically. It's not > portable, but it's one of the most portable bits of non-portable code > around, thanks to all the work you have put into it. > > Not only that, but I want to get it in ASAP so that future SRFIs can > define procedures that take keywords. Agreed. We already have several draft SRFIs and pre-SRFIs that could benefit. I feel a bit uneasy about "rushing" a new language feature in. But it's only new in standard Scheme - the territory has been thoroughly explored by many Scheme implementations, other Lisp dialects and other languages. If there were obvious alternative, I'd advocate going slower and checking them out. But what would those be? - Passing keywords as alists/hashtables is an obvious hack that is already being done too much IMHO, including in my own code. I've never felt good about it. - Defining new record types for individual procedures is just the kind of verbosity that Lisp and dynamic typing are supposed to help avoid. - Defining a macro that parses its own keywords instead of a standardized keyword procedure is definitely not simpler or less error-prone. - Defining a little domain-specific language made of simple combinators is laudable, but requires a lot of thought and design. It's not a "set-and-forget" kind of thing like keywords are. IMHO programming-in-the-large (especially application programming) is not tenable if so much thought has to be put into every component of the application. Even if Scheme is beautiful, there are times in a large project when you just have to get a job done and move on. > * default values for keyword arguments > > An alternative would be to create a singleton object, use that as the > default, and expose a trivial predicate keyword-default-value?. But #f > is probably good enough: Scheme conventionally uses it as a null anyhow. Gambit uses a custom (macro-absent-obj) a lot in its internals. The resulting code so is verbose that it's not really suitable for user code, and indeed Gambit doesn't even advertise it as a tool for users. I used to admire Common Lisp's comprehensive lambda list syntax. Then I actually used it extensively and came to the opposite conclusion. I used to think e.g. that supplied-p parameters for optional and keyword arguments are a good thing, as are custom default values. Now I think both are examples of over-engineering. In large programs, or when switching between different programming languages instead of dedicating your whole programming life to Common Lisp, you keep forgetting how the things behave. Using #f as a default for everything is extremely simple to remember and composes naturally. I have nothing against having more complex stuff in Scheme, but for my own code the kind of simplicity currently in SRFI 177 is actively desirable. The nice thing about keyword systems is that interoperability is achievable, so I can keep using a simple lambda/kw and still call other people's code written using a complex one. > * mixing keyword arguments with positional optional arguments > * mixing keyword arguments with a rest argument > > IMO both of these are bogus things to do. If you add a new optional > argument to a CL function, for example, you break all existing calls on > that function, because the keyword becomes the value of the new argument > and everything else is just wrong after that, but at least you probably > get an error. Add *two* new optional arguments, and the keyword > mentioned first just disappears. CL programmers have to filter out the > keyword-value pairs from &rest by hand if they use both. I agree with your judgement, but others want them. Since these features are all opt-in and interoperable with the simpler system, I have no a problem with them. > * allow-other-keys (collecting unknown keywords into a list) > > This is the only real nice-to-have that I see from your list, but > certainly one can do a lot without it. Agreed, and agreed. > Another good thing about SRFI 177 is that it finesses CL-style vs. > Racket-style keywords, which I would like to avoid debating if possible. Thanks. It was borne out of necessity. IMHO any keyword system that isn't fully compatible with all the existing ones is not worth attempting. One of my favorite things about Scheme is that it's easy to fit in your brain. It doesn't have countless esoteric features like CL. We can only preserve Scheme's advantage if we fold incompatible variations in language features into common subsets. R6RS and R7RS did that admirably with module systems. We can do the same with keywords.