SRFI 177

Title

Portable keyword arguments

Author

Lassi Kortela

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-177@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

Many Scheme implementations have keyword arguments, but they have not been widely standardized. This SRFI defines the macros keyword-lambda and keyword-call. They can be used identically in every major implementation currently in use, making it safe to use keyword arguments in portable code. The macros expand to native keyword arguments in Schemes that have them, letting programmers mix portable code and implementation-specific code.

Table of contents

Rationale

Why keywords?

If you have a procedure with ten parameters, you probably missed some. —Alan Perlis

Keyword arguments are a very useful tool for managing complexity as programs grow. They are a natural solution to the "no, wait, this procedure still needs another argument" problem which is almost guaranteed to pop up many times over the lifetime of any non-trivial program. Humans simply cannot plan years ahead at this level of detail, and adding keyword arguments as an afterthought is less objectionable than accumulating long lists of optional positional arguments or refactoring central APIs every few years when third-party code depends on them.

While the same information can be passed using records, keyword arguments are more convenient for simple jobs since a separate record type does not have to be defined for each procedure taking them. They are also less verbose at the call site since an extra make-foo-record call is not needed.

Standardization

Scheme implementations with native keywords

These syntaxes work with the default settings of each Scheme at the time of writing. A couple of them optionally support an alternative read syntax for keywords.

Implementation Defining keword arguments Supplying them
Chicken (lambda (foo #!key bar) ...) bar:
Gambit (lambda (foo #!key bar) ...) bar:
Kawa (lambda (foo #!key bar) ...) bar:
Bigloo (lambda (foo #!key bar) ...) :bar
Gauche (lambda (foo :key (bar #f)) ...) :bar
Sagittarius (lambda (foo :key (bar #f)) ...) :bar
STklos (lambda (foo :key (bar #f)) ...) :bar
S7 (lambda* (foo (bar #f)) ...) :bar
Guile (lambda* (foo #:key bar) ...) #:bar
Racket (lambda (foo #:bar (bar #f)) ...) #:bar

A portable approach

Procedure calls

From the above table it is clear that there is no portable syntax to supply keyword arguments in a procedure call. While all of the implementations support the syntax (proc arg1 arg2 #:key3 arg3 #:key4 arg4) with the keys in any order, the keyword prefix or suffix #: varies. On the other hand the implementations all have a low-level macro system, as well as procedures to turn symbols into keywords. Hence the easiest portable approach is to provide a macro that lets the caller use ordinary symbols instead of keywords for the read syntax. The symbols are then converted into the implementation’s native keyword syntax by the macro. The keyword-call procedure in this SRFI implements this pattern.

Procedure definitions

The next problem is how to define procedures that take keyword arguments. Portable code cannot use any syntax that requires keyword objects in the lambda list, since as noted above there is no portable representation. Here too we need to use symbols in place of keywords. For most of the implementations with native keywords, we need to insert the constant #!key or #:key into the lambda list. This turns out to be trivially easy in all of them. For a few implementations, we need to turn each keyword symbol bar into (bar #f) to ensure that the default value is #f instead of undefined. Finally, Racket requires an exotic syntax #:bar (bar #f) where the keyword’s name appears twice (once as a keyword and again as a symbol). This is easy to code as well.

Keywords as objects vs syntactic markers

Most Scheme implementations with keywords follow Common Lisp's approach and treat them as symbol-like objects. Racket and Kawa take another approach, and treat (unquoted) keywords as syntactic markers signifying a keyword argument inside a procedure call. In these dialects of Scheme, it is a syntax error to have an unquoted keyword in source code apart from valid locations inside a procedure call.

Emulating keywords in Schemes that don’t have them

For Schemes that don’t have keyword arguments, the best approach is to use a rest argument as in (lambda (arg1 arg2 . kvs) ...​). This argument takes a property list of keywords and their values. As above, the keywords can be represented by standard Scheme symbols.

Another approach would be to pass the keywords first: (lambda (kvs arg1 arg2) ...​). This is not ideal since it’s really nice to be able to call a keyword procedure just like an ordinary procedure when you don’t supply any keyword arguments (i.e. they get default values). Using a rest argument preserves this property; using one or more preceding arguments does not. Using the rest argument for keywords implies that users cannot have their own rest argument or optional positional arguments. That's fine; the interplay of keywords with optional and rest arguments is somewhat confusing to most people anyway. And our keyword arguments are all optional, so a keyword argument can be used for any purpose an optional or rest argument might.

The keyword arguments could be passed in a vector. We cannot do this because it’s not portable to use a vector as the rest argument to a procedure; it has to be a list.

R5RS has the venerable syntax-rules pattern-matching hygienic macro system. It turns out to be powerful enough to implement keyword arguments without too much effort or duct tape. Since syntax-rules pattern matching is based mainly on list structure and we don’t have any portable keyword syntax, we have to rely solely on list structure to separate positional arguments from keyword arguments. With the ubiquitous tail patterns from SRFI 46 (Basic syntax-rules extensions) we can check whether the last element of a list is another list. Consequently we can achieve the syntax (keyword-lambda (pos1 pos2 (key3 key4)) ...​) and (keyword-call pos1 pos2 (key3 key4)). This is reasonably simple and unambiguous.

Specification

Summary

This SRFI provides keyword-lambda and keyword-call which are used as follows:

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

(foo 1 2)                             ; => (1 2 #f #f #f)
(apply foo 1 2 '())                   ; => (1 2 #f #f #f)
(keyword-call foo 1 2 ())             ; => (1 2 #f #f #f)
(keyword-call foo 1 2 (d 4))          ; => (1 2 #f 4 #f)
(keyword-call foo 1 2 (d 4 e 5))      ; => (1 2 #f 4 5)
(keyword-call foo 1 2 (e 5 c 3 d 4))  ; => (1 2 3 4 5)

Details

Syntax (keyword-lambda (formals...​ (keywords...​)) body...​)

Like lambda, but makes a procedure that can take keyword arguments in addition to positional arguments.

formals are zero or more symbols naming positional arguments. keywords are zero or more symbols naming keyword arguments. All positional arguments need to be supplied by the caller on every call. There is no way to define optional positional arguments or a rest argument. By contrast, all keyword arguments are optional. Every keyword argument takes on the default value #f when no value is supplied by the caller. There is no support for user-defined default values; this simplifies the syntax of keyword lambdas and makes it easier to write wrapper procedures for them since the wrapper can always pass #f to any keyword argument it does not use.

body is evaluated as if in the context (lambda (...​) body...​) so anything that can go at the beginning of a lambda can go at the beginning of body. But the lambda around body may be wrapped inside another lambda depending on the implementation. Within body, all of the arguments in formals and keywords can be accessed as variables using the symbols given by the user.

The returned procedure p can be called in any way that an ordinary procedure can, e.g. (p args...​) or (apply p args). If the number of arguments given to the procedure is equal to the number of formals, all keyword arguments take on the default value #f. Giving more arguments than that results in undefined behavior. To supply a value for one or more keyword arguments in a well-defined and portable way, call p via (keyword-call p formals ...​ (keyword1 value1 ...​)). There may be additional implementation-defined ways to call p.

Syntax (keyword-call kw-lambda args...​ (kw-args...​))

This macro expands to a call to kw-lambda with zero or more positional arguments args and zero or more keyword arguments kw-args. It is an error to pass a different number of args than the number of positional arguments expected by kw-lambda.

Keyword arguments may be passed in any combination, and in any order. It is an error to have duplicate keywords in kw-args. As with ordinary procedure calls in Scheme, nothing is guaranteed about the evaluation order of args and kw-args. For a guaranteed evaluation order, bind arguments with let* before passing them in.

Each keyword argument in kw-args is written as two forms: keyword value. Each keyword is written as a standard Scheme symbol and is implicitly quoted (i.e. it has to be a symbol at read time; it cannot be some other expression that evaluates to a symbol). The symbols are internally converted to native keywords in implementations that have them.

Implementation

The sample implementation covers the following Scheme implementations that each have their own kind of native keywords:

There is also a generic R5RS implementation (relying on SRFI 46 tail patterns). This is used as the basis of generic R6RS and R7RS libraries for Schemes that don’t have native keywords.

The generic R6RS library works on the following Schemes:

The generic R7RS library works on the following Schemes:

Acknowledgements

Thanks to many Scheme implementors and standardization people on the srfi-discuss mailing list for partaking in a big discussion about keyword unification in July 2019. Although the problem of unifying keyword syntax and semantics proved too complex to solve in the near future, the discussion spurred this SRFI to solve the most urgent problem of letting people write portable code.

Special thanks to Robert Strandh, author of the SICL Common Lisp implementation, for taking the time to explain compiler optimization of keyword argument passing to us Schemers.

Thanks to John Cowan and Shiro Kawai for surveys of the native keyword syntax and semantics of different Scheme implementations.

Thanks to Göran Weinholt for collaborating on Docker containers that make it easy to work with exotic Scheme implementations.

Copyright

Copyright © Lassi Kortela (2019)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler