Python vs Scheme: function parameters (part II)

In part I, I looked at the handling of function arguments as specified in R5RS. Chicken adds three more ways to handle special arguments, using the keywords #!optional, #!rest and #!key. (Here's the relevant page in the Chicken documentation.)

The way I understand it, #!rest is just another way to collect optional arguments in a list. For our purposes, the following code is equivalent to (f a b . args):

> (define (r a b #!rest args)
     (list a b args))
> (r 1 2)
(1 2 ())
> (r 1 2 3)
(1 2 (3))

Optional arguments can also be specified separately (rather than collecting them all in a list). For this, use the #!optional keyword, followed by a list of names. Arguments passed to the function are associated with these names based on position. If not specified, the name defaults to #f.

> (define (o a #!optional b c)
     (list a b c))
> (o 1)
(1 #f #f)
> (o 2 'fred)
(2 fred #f)

;; roughly equivalent to Python:
;; def o(a, b=None, c=None): ...

We can also specify defaults:

> (define (p #!optional (a 42) (b 'cookie))
     (list a b))
> (p)
(42 cookie)
> (p 103)
(103 cookie)

;; roughly equivalent to Python:
;; def p(a=42, b="cookie"): ...

Again, values are associated with names based on position.

It would be useful if we could specify a parameter's name. What if, in the above example, we want to override b but not a? In that case, we need to use #!key. This works much like #!optional, except that we can specify names (using #:name syntax). In fact, we *must* specify names, because positional arguments are ignored (unless we also specify #!optional or #!rest).

> (define (k #!key (a 42) (b 'cookie))
        (list a b))
> (k)
(42 cookie)
> (k 1)
(42 cookie)  ;; neither a nor b is 1
> (k #:b 'possum)
(42 possum)
> (k #:b 'possum #:a 99) ; order of args doesn't matter
(99 possum)
> (k b: 'possum) ; this is also allowed
(42 possum)

So, in order to set a value for a, we must use #a: value in the function call. value: is also allowed.

Note if a function specifies both #!key and #!rest, keyword arguments passed to the function will show up in the rest argument as well, no matter whether there's a matching name in #!key or not.

> (define (z #!rest rest #!key (a 42) (b 'cookie))
     (list a b rest))
> (z)
(42 cookie ())
> (z 1 2)
(42 cookie (1 2))
> (z #:b 'soup 52)
(42 soup (b: soup 52))
> (z #:a 10 #:q 129)
(10 cookie (a: 10 q: 129))

In fact, using just #!rest, we can emulate Python's **kwargs construct (sort of):

> (define (y #!rest r) r)
> (y #:a 3)
(a: 3)
> (y 1 2 3 #:foo 'bar)
(1 2 3 foo: bar)

("Parsing" this list of rest args requires a bit of special code to collect the pairs; at this point, I'm not sure if Chicken provides such a function out of the box.)

By the way, we can pass the resulting list to apply without problems:

> (apply y '(1 2 3))
(1 2 3)
> (apply y '(1 2 3 foo: bar))
(1 2 3 foo: bar)
> (apply y '(1 2 3 #:foo bar))
(1 2 3 foo: bar)

What I like about the Chicken construct is that it doesn't mix up positional and keyword arguments (unlike Python, although there's a PEP to fix this in Python 3000).

Anyway, while all this is powerful, it's probably generally a good idea not to make argument lists too complicated. (The same is true in Python, by the way.)

Leave a Comment