Variable arity

A procedure's arity is the number of arguments it takes. In each of the procedures we have defined so far, we have started with the framework

(lambda (parameter1 parameter2 ... parameterN)
  ...
)

That is, we specify exactly what parameters a procedure will have; the number of these parameters (N) is fixed. For example, the arity of the cons procedure is 2, and the arity of the predicate char-uppercase? is 1.

However, not all of the procedures we have encountered have this property. For example, Scheme's procedure's list, +, and string-append allow any number of parameters. We say such procedures have variable arity. 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 their arity varies from one call to another. In what follows, we explore how these procedures of variable arity can be defined.

Zero or More Parameters

As a first example, consider how we might define a procedure to add an unlimited number of values. Of course, the procedure + is already built into Scheme, but we consider how to write add ourselves assuming that + applies just to two numbers. Thus, add should return the following results:

Expression Value Returned
(add 3 1) 4
(add 3 1 4 1 5)14
(add)0

The following procedure illustrates how this procedure add could be written:

;;; add:  compute the sum of the values that follow

;;; Givens:
;;;    Some number of values, collectively called VALUES

;;; Result:
;;;    SUM, a number

;;; Preconditions:
;;;    all VALUES are numbers

;;; Postcondition:
;;;    SUM is the result of adding the numbers in VALUES

(define add
   (lambda values
      (let loop ((lst values))
          (if (null? lst)
              0
              (+ (car lst) (loop (cdr lst)))
          )
      )
   )
)

In this approach, note that the symbol values after lambda is not in parentheses. Scheme interprets this syntax to indicate that all parameters to the procedure should be grouped together in a list. Thus, in the procedure call (sum 3 1 4 1 5), Scheme interprets the parameter values to be the list (3 1 4 1 5).

One or More Parameters

Next, consider the task of editing a sequence of numbers, removing negative numbers and those larger than a specified maximum. In this case, a first parameter will be required to state the specified maximum, and all subsequent values will be scanned to determine which are in range. The following table illustrates how this filter-1 procedure should work:

Expression Value Returned
(filter-1 3 6 2 -1) (2)
(filter-1 100 -10 -1 0 1 10 100 1000) (0 1 10 100)
(filter-1 100) ()
(filter-1) Error: parameter missing

In this case, filter-1 only makes sense if it contains a first parameter, but we cannot anticipate how many parameters will occur after that. Another form of lambda expressions allows us to bind initial parameters to specific symbols, with any remaining parameters collected together in a list. This is illustrated in the following code:

;;; filter-1:  filter negatives and large numbers from a number sequence

;;; Givens:
;;;    A MAXIMUM allowed value, followed by
;;;    Some number of values, collectively called VALUES

;;; Result:
;;;    LS:  a list of numbers

;;; Preconditions:
;;;    MAXIMUM and all VALUES are numbers

;;; Postcondition:
;;;    LS contains all numbers in VALUES, provided the numbers are
;;;    non-negative and do not exceed MAXIMUM.  The order of numbers
;;;    on LS is the same as the order in VALUES.

(define filter-1
   (lambda (maximum . values)
      (let loop ((lst values) (result-so-far '()))
          (cond ((null? lst) (reverse result-so-far))
                ((<= 0 (car lst) maximum) 
                       (loop (cdr lst) (cons (car lst) result-so-far)))
                (else (loop (cdr lst) result-so-far))
          )
      )
   )
)

Here, (maximum . values) follows lambda. In this context, the first parameter is bound to maximum,while all remaining parameters are placed on a list and bound to values.

Two or More Parameters

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.

As an example, the following refinement of the previous filtering procedures asks the user to specify both the minimum and maximum values for filtering subsequent values.

;;; filter-2:  filter values below a minimum or above a maximum from a number sequence

;;; Givens:
;;;    A MINIMUM and a MAXIMUM allowed value, followed by
;;;    Some number of values, collectively called VALUES

;;; Result:
;;;    LS:  a list of numbers

;;; Preconditions:
;;;    MINIMUM, MAXIMUM, and all VALUES are numbers

;;; Postcondition:
;;;    LS contains all numbers in VALUES, provided the numbers are
;;;    not below MINIMUM do not exceed MAXIMUM.  The order of numbers
;;;    on LS is the same as the order in VALUES.

(define filter-2
   (lambda (minimum maximum . values)
      (let loop ((lst values) (result-so-far '()))
          (cond ((null? lst) (reverse result-so-far))
                ((<= minimum (car lst) maximum) 
                       (loop (cdr lst) (cons (car lst) result-so-far)))
                (else (loop (cdr lst) result-so-far))
          )
      )
   )
)

This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~walker/courses/151.sp04/readings/variable-arity.xhtml

created March 22, 1997
last revised March 9, 2004

Henry Walker (walker@cs.grinnell.edu), John David Stone (stone@cs.grinnell.edu), and Ben Gum (gum@cs.grinnell.edu)