SRFI l -*- outline -*- * Title Optional procedure arguments * Author Taylor Campbell * Abstract This proposal extends the LAMBDA syntax to allow for optional arguments along with the current rest arguments. This simplifies the definitions of many procedures that are designed to have optional arguments, but that must, in the status quo, either use a complicated arity dispatcher or destructure argument rest-lists. It is similar in semantics to DSSSL's #!OPTIONAL token and Common Lisp's &OPTIONAL token in lambda lists, but it differs slightly in its functionality and syntax. * Issues ** Syntax The precise syntax for specifying optional arguments is questionable. I'm not sure that the extra parenthetical tokens suffice to indicate optional arguments clearly, and it gives optional arguments a special status that could not be offered to later extensions, such as keyword arguments or destructuring LAMBDA lists. The most common way to do this is to introduce a special #!OPTIONAL token. This generally reads either as an object of a new, disjoint type, which is used in no other context, or a symbol. The new disjoint type is a wart because it serves no other purpose but a very specific one in the syntax of LAMBDA, yet it is an externally serializable datum so it can be stored in an arbitrary S-expression, in which it ought to at least serve some useful general purpose. A symbol as in Common Lisp could mark the start of optional parameters, as long as it is a suitably different symbol from what one might want to bind as a variable, but even then it might break existing code, and it is not necessary in the first place, because the form of optional optional parameters is already sufficiently different from that of required parameters, the former being two- or three-element lists and the latter being simple symbols. Another potential alternative for marking optional arguments is to wrap all of the optional argument specifiers in an (OPTIONAL ...); e.g., (define (f a b c (optional (x 'absent-x) (y 'absent-y y-present?)) . rest) ...) * Rationale Many Scheme procedures accept a fixed number of optional arguments, such as MAKE-VECTOR, READ-CHAR, and SRFI 13's STRING-COPY. However, actually writing procedures that accept optional arguments is tedious and wasteful with only R5RS's 'rest argument' mechanism: not only must one meticulously destructure the rest argument list or make complicated macros to do so, but this is also very inefficient, as argument parsing becomes heavy in heap-allocated argument list manipulation for all but the most optimizing of Scheme implementations. Argument passing is usually just a few register moves or stack pushes; using argument lists necessitates deep heap accessing. In SRFI 16, a new form, CASE-LAMBDA, was proposed for procedures whose arities are variable, but it, too, is clumsy for simple optional arguments, as it is really convenient only for procedures whose semantics differ significantly with the varying arities. See the 'Related work' section for more details. * Specification The production of R5RS's formal syntax in section 7.1.3 is modified to be: ---> (* *) | | (+ * . ) where ---> ( ) | ( ) Semantics: In calls to procedures constructed by LAMBDAs with optional parameter specifiers, each optional parameter in left-to-right sequence is processed as follows: - If it is of form ( ), is bound to a fresh location. If the corresponding argument is present, that 's location is initialized to be its value; otherwise, it is initialized to be the value of . , if evaluated, is evaluated in a lexical environment in which all of the previous variables in the formal parameter list have been bound and initialized. - Otherwise, it must be of the form ( ). In this form, both & are bound to fresh locations. If the argument in the call corresponding with that parameter is present, then is initialized to the value of that argument, and is initialized to be the true constant #T; otherwise, 's location is initialized to be the value of in a lexical environment in which all of the previous variables in the formal parameter list have been bound and initialized, and 's location is initialized to the false constant #F. A formal denotational semantics for the new LAMBDA form is forthcoming. * Examples (define (f a b c (x 'absent-x) (y 'absent-y y-present?) . rest) (list a b c x y y-present? rest)) (f 1 2 3) ; => (1 2 3 ABSENT-X ABSENT-Y #F ()) (f 1 2 3 'a 'b) ; => (1 2 3 A B #T ()) (f 1 2 3 'a 'b 'c 3.14 2.71 0) ; => (1 2 3 A B #T (C 3.14 2.71 0)) (define (maybe-read-char (port (current-input-port))) ;; This uses Taylor Campbell's proposed extension to COND. (cond ((read-char port) char? => values) (else #f))) (define (vector-copy! target tstart tend source (sstart 0) (send (vector-length source))) (do ((target-index tstart (+ target-index 1)) (source-index sstart (+ source-index 1))) ((= target-index tend)) (vector-set! target target-index (vector-ref source source-index)))) (define (vector-copy vector (start 0) (end (vector-length vector))) (let* ((length (- end start)) (result (make-vector length))) (vector-copy! result 0 length vector start) result)) * Related work ** DSSSL: Document Style Semantics and Specification Language DSSSL, a restricted dialect of Scheme for processing SGML, extended the LAMBDA special form to accept optional arguments. (It also supported an alternate rest argument mechanism and named keyword arguments, but they are not relevant here.) A formal paramter list could contain an #!OPTIONAL token, which signified that the following parameters were optional. Formal parameters that were optional were specified in one of two ways: with an explicit default value expression, to which the variable for that formal parameter would be initialized; or without an explicit default value expression, in which case the parameter's default value would be implicitly #F. For example, (define (foo a b #!optional c (d 3)) (list a b c d)) (foo 5 3) => (5 3 #f 3) (foo 5 3 1) => (5 3 1 3) (foo 5 3 1 'fnord) => (5 3 1 FNORD) There are two differences between DSSSL's facility and this proposal. The first is in syntax: whereas DSSSL requires the introduction of a special new 'named constant' data type, which is disjoint from all other types and of which there are only three instances (#!OPTIONAL, #!REST, & #!KEY). This data type serves no useful purpose except in formal parameter lists, and it was deemed to be an unnecessary wart when this proposal was designed. The second difference is in ability to test for argument presence, which is frequently a useful operation. DSSSL provides a facility only for specifying a particular default value for each optional parameter, or merely listing an optional parameter whose default value will be specified implicitly as #F. There is no way to determine later whether an argument was actually present except by using a token as the default value that is known not to be in the procedure's actual domain. This is a cumbersome way to implement what is really a very straightforward operation. In this SRFI, one can specify either a default value or a default value and a variable to bind to true or false depending on whether the argument was supplied or not. This does not allow the case of an implicit default value of #F, as is explained in the next section, but it does allow simple conditionalization on the presence of optional arguments. Otherwise, except for syntax, implicit default values, and argument presence variables, DSSSL's optional argument semantics is identical to this proposal's. ** Common Lisp Common Lisp's facility is almost identical to this proposal, except for syntax and that optional arguments can be specified with no explicit default value, in which case they are initialized to NIL if no corresponding argument is supplied. In Scheme, there is no similar distinguished NIL value, since the null list is disjoint from the false constant, and so there is no equivalent obvious value for an implicit default value of optional arguments; it was deemed clearer always to specify a default value explicitly. Common Lisp, too, like DSSSL, uses a special token in the argument list, but this is only a certain symbol from the COMMON-LISP package, not a new data type. ** SRFI 16: CASE-LAMBDA SRFI 16's CASE-LAMBDA has been proposed as a mechanism for procedures of variable arities, but it is intended more for procedures whose semantics vary significantly for each of its distinct possible arities. Using CASE-LAMBDA to emulate optional arguments requires that one write either a recursive procedure to repeatedly pass default optional arguments to or several auxiliary procedures to handle the work. For example, SRFI 13's STRING-COPY might be written thus using CASE-LAMBDA and recursively calling itself with default values for its optional arguments: (define string-copy (case-lambda ((string) (string-copy string 0)) ((string start) (string-copy string start (string-length string))) ((string start end) (let* ((length (- end start)) (result (make-string length))) (do ((i start (+ i 1)) (j 0 (+ j 1))) ((= j length) result) (string-set! result j (string-ref string i))))))) It might alternatively be written using a number of small procedures to handle the defaulting individually: (define string-copy (let* ((string-copy (lambda (string start end) (let* ((length (- end start)) (result (make-string length))) (do ((i start (+ i 1)) (j 0 (+ j 1))) ((= j length) result) (string-set! result j (string-ref string i)))))) (string-copy/default-end (lambda (string start) (string-copy string start (string-length string)))) (string-copy/default-start (lambda (string) (string-copy/default-end string 0)))) (case-lambda ((string) (string-copy/default-start string)) ((string start) (string-copy/default-end string start)) ((string start end) (string-copy string start end))))) but this amounts to roughly the same procedure. In either style, most of the procedure's content is occupied with boilerplate optional argument defaulting. With this proposal, STRING-COPY could be written succinctly as simply: (define (string-copy string (start 0) (end (string-length string))) (let* ((length (- end start)) (result (make-string length))) (do ((i start (+ i 1)) (j 0 (+ j 1))) ((= j length) result) (string-set! result j (string-ref string i))))) There is clearly a significant difference for this very common pattern between CASE-LAMBDA and this proposal for an optional argument system. Note, however, that the pattern of the latter use of CASE-LAMBDA may nevertheless be a tractable implementation technique for LAMBDAs that accept optional arguments. See the 'Implementation' section for more details on this. ** Shivers' LET-OPTIONALS Olin Shivers wrote a very sophisticated LET-OPTIONALS macro to perform careful destructuring of R5RS rest argument lists, interleaved with argument checks and easily integrated with external argument parsers, such as SRFI 13's STRING-PARSE-START+END. However, not only is it a very complicated macro that is difficult to understand and port to macro systems beyond that for which it was written, but it furthermore uses the overly simple rest argument mechanism of R5RS, which has many fundamental flaws for optional arguments. In particular, the macro's expansion is very complicated with many nested conditionals and heap accesses, which is simply unreasonable for argument passing. Even Shivers himself points out in some of his code that calling a procedure should just involve some register moves or stack pushes, and that it is ludicrous to parse heap-allocated rest arguments -- which must be copied at every procedure call, too -- to emulate an optional argument facility. Furthermore, it requires a very sophisticated macro to not redundantly operate on the heap-allocated lists; there exist naive implementations of LET-OPTIONALS that are much easier to comprehend (and that are easy to write using only SYNTAX-RULES), but they are very inefficient with respect to redundant heap accesses, which are bad enough to be needed at all for argument passing, even if not redundant. Moreover, not only is the heap manipulation required to parse rest arguments inefficient at run-time, but such parsing is also very difficult for a compiler to analyze effectively and optimize. To demonstrate use of LET-OPTIONALS for comparison with the mechanism here proposed, here is the STRING-COPY procedure above, but written to use rest arguments and LET-OPTIONALS: (define (string-copy string . start+end) (let-optionals start+end ((start 0) (end (string-length string))) (let* ((length (- end start)) (result (make-string length))) (do ((i start (+ i 1)) (j 0 (+ j 1))) ((= j length) result) (string-set! result j (string-ref string i)))))) It is interesting to note that this use of LET-OPTIONALS expands almost exactly by Shivers' macros to the second example of using CASE-LAMBDA above, only with manual argument destructuring instead of dispatching of arity via CASE-LAMBDA. Further discussion of this pattern is in the 'Implementation' section. * Implementation Implementing optional arguments efficiently is usually a matter of dispatching on the number of arguments and then repeatedly installing default values for the subsequent arguments. If we assume an efficient implementation of CASE-LAMBDA, transforming optional arguments into uses of it is trivial. The LAMBDA expands to a set of local LAMBDAs along with one dispatching CASE-LAMBDA, in the style as shown in the STRING-COPY example of the comparison between this proposal and CASE-LAMBDA as an optional argument mechanism. The CASE-LAMBDA dispatches to the local wrapper procedure with each possible number of arguments presented. In the STRING-COPY example, there are two such wrapper procedures: STRING-COPY/DEFAULT-END and STRING-COPY/DEFAULT-START. The resultant CASE-LAMBDA-created closure dispatches on the number of arguments: if given one argument, it passes control to STRING-COPY/DEFAULT-START, which then gives a default value for the START argument and passes on to STRING-COPY/DEFAULT-END; this in turn then enters the main body of STRING-COPY with the last optional argument's default value. If the procedure is given two arguments, it enters STRING-COPY/DEFAULT-END with those two arguments, and then STRING-COPY/DEFAULT-END moves on to the main body. Finally, if given three arguments, control is dispatched directly to the main body. As another example, this one somewhat more convoluted, the F procedure, defined with this proposal with (define (f a b c (x 'absent-x) (y 'absent-y y-present?) . rest) (list a b c x y y-present? rest)) might be rewritten with CASE-LAMBDA as (define f (let* ((f (lambda (a b c x y y-present? . rest) (list a b c x y y-present? rest))) (f/y (lambda (a b c x) (f a b c x 'absent-y #f))) (f/x (lambda (a b c) (f/y a b c 'absent-x)))) (case-lambda ((a b c) (f/x a b c)) ((a b c x) (f/y a b c x)) ((a b c x y . rest) (apply f a b c x y #t rest))))) At an even lower level than CASE-LAMBDA, optional argument dispatch can be accomplished with a simple pseudo-computed-GOTO sequence. Here is the STRING-COPY example translated into assembly pseudo-code, where A1 contains the first argument, A2 the second, &c.; NARGS contains the number of arguments; and #... is a literal constant: ;;; A1 contains the STRING parameter; A2, START; & A3, END. cmp NARGS,#1 ; Compare NARGS to the immediate 1. j< lose ; If <1 argument, lose. ;;; Pseudo-computed-GOTO sequence to argument-defaulting labels: j= start ; If 1 argument, go to START label. cmp NARGS,#2 ; Compare NARGS to the immediate 2. j= end ; If 2 arguments, go to END label. cmp NARGS,#3 ; Compare NARGS to the immediate 3. j= body ; If 3 arguments, go to the main body. j> lose ; If >3 arguments, go to the lossage label. ;;; Default argument selection: start load A2,#0 ; Load 0 into A2 (START argument) and fall ; through to the END defaulting. end load A3,-1(A1) ; Load the -1th slot of A1 (the string), ; which we assume holds the length of the ; string, into A3, the END argument; fall ; through to the main body. body ...run the loop... return ;;; We jump here if there's a wrong number of arguments. lose load A1,#"wrong number of arguments" call ERROR ; Call the system error signaller. * Copyright Copyright (C) 2004, 2005 Taylor Campbell. All rights reserved. Don't distribute this. I'm not liable; if stuff goes wrong, it's all your fault, *nyah nyah*. This copyright notice will change in the real SRFI document to something reasonable (in particular, the official SRFI copyright notice, surprise surprise).