Variable arity

Course links

A procedure's arity is the number of arguments it takes. You'll probably have noticed that while some of Scheme's built-in procedures always take the same number of arguments (for instance, the arity of the cons procedure is 2, and the arity of the predicate char-uppercase? is 1), others have variable arity -- that is, the number of arguments in a call can vary. Such Scheme procedures as list, +, or string-append can take any number of arguments.

Still other Scheme procedures, such as map, require at least a certain number of arguments, but will accept one or more additional arguments if they are provided. For example, the arity of map is ``2 or more.'' These procedures, too, are said to have variable arity, because the number of arguments varies from one call to another.

It is possible for the programmer to define new variable-arity procedures in Scheme, by using alternate forms of the lambda-expression.

In all of the programmer-defined procedures that we have seen so far, the keyword lambda has been followed by a list of parameters -- names for the values that will be supplied by the caller when the procedure is invoked. If, instead, what follows lambda is a single identifier -- not a list, but a simple identifier, not enclosed in parentheses -- then the procedure denoted by the lambda-expression will accept any number of arguments, and the identifier following lambda will name a list of all the arguments supplied in the call.

As an example, let's develop a procedure called slam that works like a combination of cons and append: It takes any number of arguments, some or all of which may be lists, and concatenates them; but when it finds an argument that is not a list, it just puts it in as an element of the list it returns.

Especially since the behavior I've described is a little weird, we should begin by writing a lot of sample calls, making sure that we know what we want to have happen in each case:

> (slam)               ; no arguments: result should be null
()
> (slam 'alpha)        ; one argument, not a list
(alpha)
> (slam '(beta))       ; one argument, a list    
(beta)
> (slam 'gamma 'delta 'epsilon)    ; non-lists only
(gamma delta epsilon)
> (slam '(zeta) '(eta theta) '() '(iota kappa mu nu))  ; lists only
(zeta eta theta iota kappa mu nu)
> (slam 'omicron '(pi rho) 'sigma 'tau '(upsilon))
(omicron pi rho sigma tau upsilon)

The following examples illustrate the point that only the top-level lists are ``opened up'' to get elements for the result list:

> (slam 1 2 '(3 4) '((5 6) (7 8)) '(((9 10) (11 12))))
(1 2 3 4 (5 6) (7 8) ((9 10) (11 12)))
> (slam 1 '(2) 3 '((4)) 5 '(((6))))
(1 2 3 (4) 5 ((6)))
> (slam 1 '() 2 '() 3)
(1 2 3)
> (slam 1 '(()) 2 '((())) 3)
(1 () 2 (()) 3)
> (slam '() '() '() '() '())
()
> (slam '() '(()) '(())) '(((()))))
(() (()) ((())))

Since slam can take any number of arguments, including none, the lambda-expression that we write should have an identifier after the lambda, not a list of identifiers:

(define slam
  (lambda arguments
    ...))

When slam is actually invoked, the arguments are assembled into a list, and this list is the value of arguments inside the lambda-expression. Now it's a straightforward list recursion: If arguments is empty, we return '(). Otherwise, we issue a recursive call to slam the cdr of arguments, then either append or cons the car of arguments on the front of the result, depending on whether the car of arguments is a list or not.

Since we have abstracted list recursion into the higher-order procedure fold-list, the body of the lambda-expression is easy to write:

;;; slam: assemble a list from any number of given values,
;;; extracting top-level elements from any of them that are lists

;;; Givens:
;;;   Some number of values, collectively called ARGUMENTS.

;;; Result:
;;;   LS, a list.

;;; Preconditions:
;;;   None.

;;; Postcondition:
;;;   The elements of LS are precisely the values among
;;;   ARGUMENTS that are not lists and the top-level elements
;;;   of the values among ARGUMENTS that are lists.

(define slam
  (lambda arguments
    ((fold-list '() (lambda (new base)
                       (if (list? new)
                           (append new base)
                           (cons new base))))
      arguments)))

It's a little easier to define slam in this way, using the higher-order procedure than to make the recursion explicit. For comparison, here's what slam looks like with explicit recursion instead of the call to fold-list:

(define slam
  (lambda arguments
    (if (null? arguments)
        '()
        (let ((new (car arguments))
              (base (apply slam (cdr arguments))))
          (if (list? new)
              (append new base)
              (cons new base))))))

The tricky part here is to think of writing (apply slam (cdr arguments)) rather than simply (slam (cdr arguments)). Remember, the variable-arity construction has the effect of collecting all of the arguments into a list. You need apply to make sure that, in effect, the elements of (cdr arguments) are broken out again before the recursive call is made.

If the programmer wishes to require some fixed minimum number of arguments while permitting (but not requiring) additional ones, she can use yet another form of the lambda-expression, in which a dot is placed between the last two identifiers in the parameter list. All the identifiers to the left of this dot correspond to individual required arguments. The identifier to the right of the dot designates the list of all of the remaining arguments, the ones that are optional.

For instance, let's look at the set-difference procedure, which takes one or more lists l1, l2, ..., ln as arguments and returns a list containing all of the elements of l1 that are not also elements of any of l2, ..., ln. The set-difference procedure allows the caller to supply any number of lists of values to be ``pruned out'' of an initial list:

> (set-difference '(a b c))
(a b c)
> (set-difference '(a b c d e) '())
(a b c d e)
> (set-difference '(a b c d e) '(a c))
(b d e)
> (set-difference '(a b c d) '(b e h))
(a c d)
> (set-difference '() '(a b c d))
()
> (set-difference '(a b c) '(c e b))
(a)
> (set-difference '(a b c) '(c a k b d))
()
> (set-difference '(a b c d e) '(a f) '(e b h))
(c d)
> (set-difference '(a b c d e f) '(b g) '() '(h e j p t) '(c b))
(a d f)

Here's the definition:

;;; set-difference: construct and return a list formed by
;;; removing the elements of any number of given lists from
;;; a given initial list

;;; Givens:
;;;   INITIAL, a list.
;;;   Any number of lists, collectively called OTHERS.

;;; Result:
;;;   DIFFERENCE, a list.

;;; Preconditions:
;;;   None.

;;; Postconditions:
;;;   The elements of DIFFERENCE are exactly those elements
;;;   of INITIAL that are not also elements of any of OTHERS.

(define set-difference
  (lambda (initial . others)
    ((fold-list initial
                (lambda (new recursive-result)
                  ((remove (right-section member new))
                   recursive-result)))
     others)))

In English: Call the initial list initial and collect all of the other arguments into a list called others. Using list recursion, fold over others: In the base case, where others is null, just return initial. In any other case, separate others into its car and its cdr, and issue a recursive call to deal with the cdr -- that is, prune out of initial all of the elements of elements of the cdr. From the result of this recursive call, remove any value that is a member of the car and return the result.

The dot notation can be used to specify any number of initial values. Thus, a parameter list of the form

(first-value second-value . remaining-values)

indicates that the first two arguments are required, while additional arguments will be collected into a list named remaining-values.

The principal author of this reading is Professor Henry Walker. I am also indebted to Professor Ben Gum for his contributions to its development.