Variable arity

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, they can take any number of arguments. All one can say about the arity of a Scheme procedure such as list, +, or string-append is that it is some non-negative integer.

Still other Scheme procedures, such as map and display, 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,'' and the arity of display is ``1 or 2.'' These procedures, too, are said to have variable arity, because their arity 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.

Here's a simple example: We'll define a display-line procedure that takes any number of arguments and prints out each one (by applying the display procedure to it), then terminates the output line (by invoking newline). Note that in the lambda-expression, the identifier arguments denotes a list of all the items to be printed:

(define display-line
  (lambda arguments
    (let kernel ((rest arguments))
      (if (null? rest)
          (newline)
          (begin
            (display (car rest))
            (kernel (cdr rest)))))))

When display-line is invoked, however, the caller does not assemble the items to be printed into a list, but just supplies them as arguments:

> (display-line "+--" "Here is a string!" "--+")
+--Here is a string!--+

> (display-line "ratio = " 35/15)
ratio = 7/3

Exercise 1

Try out some other calls to display-line to check what it prints. For example, try the following:

(display-line "going" "going" "gone")
(display-line "countdown:" 5 4 3 2 1 "done")
(display-line)          ;; apply display-line to no arguments

Explain your results.


Exercise 2

The current version of display-line prints all text together without spaces. Modify the code, so that one space is printed between any two adjacent values supplied as arguments to display-line. For instance, after your modifications, the output from the first call to display-line above should be

+-- Here is a string! --+

Exercise 3

Define and test a procedure named call-arity that takes any number of arguments and returns the number of arguments it received (ignoring their values):

> (call-arity 'a #\b "c" '(d))
4

> (call-arity 0.0)
1

> (call-arity)
0

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 single 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, we can define a procedure called display-separated-line that always takes at least one argument, separator, but may take any number of additional arguments. Display-separated-line will print out each of the additional arguments (by invoking display) and terminate the line, just as display-line does, but with the difference that a copy of separator will be displayed between any two of the remaining values. Here is some sample output:

> (display-separated-line "..." "going" "going" "gone")
going...going...gone

> (display-separated-line ":-" 5 4 3 2 1 'done)
5:-4:-3:-2:-1:-done

> (display-separated-line #\space "+--" "Here is a string!" "--+")
+-- Here is a string! --+

> (display-separated-line (integer->char 9) 1997 'foo 'wombat 'quux)
1997    foo     wombat  quux
; (INTEGER->CHAR 9) is the tab character.

And here is the definition of the procedure:

(define display-separated-line
  (lambda (separator . arguments)
    (if (null? arguments)
        (newline)
        (let kernel ((rest arguments))
          (display (car rest))
          (if (null? (cdr rest))
              (newline)
              (begin
                (display separator)
                (kernel (cdr rest))))))))

As another example, 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. For example, the value of

(set-difference (list 'a 'b 'c 'd 'e 'f 'g)
                (list 'a 'e) (list 'b) (list 'a 'f 'h))

is (list 'c 'd 'g), because the elements 'c, 'd, and 'g of the first argument are not elements of any of the subsequent list. The set-difference procedure allows the caller to supply any number of lists of values to be ``pruned out'' of the initial list.

Here's the definition:

(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.


Exercise 4

What happens if you invoke display-separated-line without giving it any arguments? What happens when only one argument is given?


Exercise 5

Define and test a procedure clicker that takes one or more arguments, of which the first must be an integer and each of the others must be either the symbol 'up or the symbol 'down. Clicker should start from the given integer, add 1 for each 'up argument, subtract 1 for each 'down argument, and return the result:

> (clicker 17 'up 'up)
19
> (clicker -12 'down 'up 'down 'down 'down)
-15
> (clicker 100)
100

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.


Exercise 6

Modify the definition of the display-separated-line procedure so that it takes an output port as its first (required) argument, the separator as its second (required) argument, and any number of values to be printed as additional (optional) arguments. The procedure should write the line, with separators, to the specified output port rather than to the interaction window.


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/variable-arity.xhtml

created March 22, 1997
last revised March 17, 2000

Henry Walker (walker@cs.grinnell.edu) and John David Stone (stone@cs.grinnell.edu)