Chapter 21 - Fancy Tricks with Function and Macro Arguments

We've already seen (in Chapter 4) how &OPTIONAL parameters can reduce the number of arguments that you have to supply for the most common calls of a function. In this chapter we'll look at additional language features that let you declare named (keyword) parameters and provide default values for unspecified parameters. We'll also take a look at structured argument lists, which let you group related parameters for clarity.

Keywords let you name your parameters

Sometimes you'll want to define a function (or macro) that works just fine with a small list of arguments, but can be extended in useful -- and obvious, I hope -- ways through the addition of extra arguments. But you'd rather not specify all of the arguments all of the time. We've already seen keyword arguments used in Chapter 13 with the sequence functions, and in Chapter 19 with the stream functions.

You can use keyword arguments for your own functions or macros by adding a &key marker to the lambda list. The general form (also used for DEFMACRO) is:

(defun name (req-arg ... &key key-arg)

All of the required arguments (req-arg) must precede the &KEY marker. The key-args name the variable that you'll reference from within your function's definition; the same key-arg name in the keyword package (i.e. preceded with a colon) is used in the call as a prefix for the keyword value.

? (defun keyword-sample-1 (a b c &key d e f)
    (list a b c d e f))
? (keyword-sample-1 1 2 3)
(1 2 3 NIL NIL NIL)
? (keyword-sample-1 1 2 3 :d 4)
(1 2 3 4 NIL NIL)
? (keyword-sample-1 1 2 3 :e 5)
(1 2 3 NIL 5 NIL)
? (keyword-sample-1 1 2 3 :f 6 :d 4 :e 5)
(1 2 3 4 5 6)

You'll notice from the last sample that keyword parameters may be listed in any order. However, as in their lambda list declaration, all keyword parameters must follow all required parameters.

Default values for when you'd rather not say

Any keyword parameter that you don't specify in a call receives a NIL default value. You can change the default using a variation of the keyword argument declaration: instead of just the argument name, specify (name default), like this:

? (defun keyword-sample-2 (a &key (b 77) (c 88))
    (list a b c))
? (keyword-sample-2 1)
(1 77 88)
? (keyword-sample-2 1 :c 3)
(1 77 3)

You can also find out whether a keyword parameter was specified in the call, even if it was specified using the default value. The keyword argument declaration looks like this: (name default arg-supplied-p), where arg-supplied-p is the name of a variable that your function's definition reads as NIL only if no argument is supplied in the call.

? (defun keyword-sample-3 (a &key (b nil b-p) (c 53 c-p))
    (list a b b-p c c-p))
? (keyword-sample-3 1)
(1 NIL NIL 53 NIL)
? (keyword-sample-3 1 :b 74)
(1 74 T 53 NIL)
? (keyword-sample-3 1 :b nil)
(1 NIL T 53 NIL)
? (keyword-sample-3 1 :c 9)
(1 NIL NIL 9 T)

Default values and supplied-p variable can also be used with &OPTIONAL parameters.

? (defun optional-sample-1 (a &optional (b nil b-p))
    (list a b b-p))
? (optional-sample-1 1)
? (optional-sample-1 1 nil)
(1 NIL T)
? (optional-sample-1 1 2)
(1 2 T)

If you use both &OPTIONAL and &KEY parameters, all of the optional parameters must precede all of the keyword parameters, both in the declaration and the call. Of course, the required parameters must always appear before all other parameters.

? (defun optional-keyword-sample-1 (a &optional b c &key d e)
    (list a b c d e))
? (optional-keyword-sample-1 1)
? (optional-keyword-sample-1 1 2)
? (optional-keyword-sample-1 1 2 3)
(1 2 3 NIL NIL)
? (optional-keyword-sample-1 1 2 3 :e 5)
(1 2 3 NIL 5)

When you define both &OPTIONAL and &KEY arguments, the call must include values for all of the optional parameters if it specifies any keyword parameters, as in the last sample, above. Look at what can happen if you omit some optional parameters:

? (defun optional-keyword-sample-2 (a &optional b c d &key e f)
    (list a b c d e f))
? (optional-keyword-sample-2 1 2 :e 3)
(1 2 :E 3 NIL NIL)
Even though a Common Lisp function (READ-FROM-STRING) uses both optional and keyword arguments, you should not do the same when you define your own functions or macros.

Add some structure to your macros by taking apart arguments

You can use destructuring to create groups of parameters for macros.

? (defmacro destructuring-sample-1 ((a b) (c d))
    `(list ',a ',b ',c ',d))
? (destructuring-sample-1 (1 2) (3 4))
(1 2 3 4)

You can use all the usual techniques within each group.

? (defmacro destructuring-sample-2 ((a &key b) (c &optional d))
    `(list ',a ',b ',c ',d))
? (destructuring-sample-2 (1) (3))
(1 NIL 3 NIL)
? (destructuring-sample-2 (1 :b 2) (3))
(1 2 3 NIL)
? (destructuring-sample-2 (1) (3 4))
(1 NIL 3 4)

And the groupings can even be nested.

? (defmacro destructuring-sample-3 ((a &key b) (c (d e) &optional f))
    `(list ',a ',b ',c ',d ',e ',f))
? (destructuring-sample-3 (1) (3 (4 5)))
(1 NIL 3 4 5 NIL)

Destructuring is commonly used to set off a group of arguments or declarations from the body forms in a macro. Here's an extended example, WITH-PROCESSES, that expects a name, a list of a variable name (pid) and a process count (num-processes), and a list of another variable name (work-item) and a list of elements to process (work-queue). All of these arguments are grouped before the body forms.

? (defmacro with-processes ((name 
                             (pid num-processes)
                             (work-item work-queue)) &body body)
    (let ((process-fn (gensym))
          (items (gensym))
          (items-lock (gensym)))
      `(let ((,items (copy-list ,work-queue))
             (,items-lock (make-lock)))
         (flet ((,process-fn (,pid)
                  (let ((,work-item nil))
                      (with-lock-grabbed (,items-lock)
                        (setq ,work-item (pop ,items)))
                      (when (null ,work-item)
                      ;;(format t "~&running id ~D~%" ,pid) 
           (dotimes (i ,num-processes)
             ;;(format t "~&creating id ~D~%" ,id) 
              (format nil "~A-~D" ,name i)

Processes are not part of the ANSI Common Lisp standard, but are present in almost every implementation. (We'll revisit processes in Chapter 32, along with some other common language extensions.) The code shown above works with Macintosh Common Lisp, whose process interface is very similar to that found on the Symbolics Lisp Machines of days past.

I'll describe a few key portions of the macro expander in case you want to figure out what's going on; if you'd rather just see how the macro gets called, you can skip the rest of this paragraph. The FLET form defines a function. In this case, the function defined by FLET will be used to do the actual work within a Lisp process -- grab a lock on the work queue, remove an item, release the lock, then process the item using the body forms. The PROCESS-RUN-FUNCTION creates a Lisp process with a given name (generated by the FORMAT form) and a function to execute. The WITH-PROCESSES macro creates NUM-PROCESSES Lisp processes (named name-#) and within each process executes the BODY forms with PID bound to the process number and WORK-ITEM bound to some element of WORK-QUEUE. The processes terminate themselves once the work queue has been consumed.

Here's an example of how we call WITH-PROCESSES. The parameters are "Test" (used for the process names), (id 3) (the variable bound to the process ID within a process, and the number of processes to create), and (item '(1 2 ... 15 16) (the variable bound to an individual work item within a process, and the list of items to be consumed by the processes). The FORMAT and SLEEP forms comprise the body of the processes, and the final argument to the WITH-PROCESSES macro call.

? (with-processes ("Test"
                   (id 3)
                   (item '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16)))
    (format t "~&id ~D item ~A~%" id item)
    (sleep (random 1.0)))
id 0 item 1
id 1 item 2
id 2 item 3
id 1 item 4
id 1 item 5
id 0 item 6
id 2 item 7
id 0 item 8
id 2 item 9
id 1 item 10
id 2 item 11
id 0 item 12
id 0 item 13
id 1 item 14
id 2 item 15
id 0 item 16

The form returns NIL almost immediately, but the created processes run for a while to produce the output that follows. The "item" numbers follow an orderly progression as they are consumed from the work queue, but the "id" numbers vary according to which process actually consumed a particular item.

Destructuring is a useful tool for macros, but you can't use it in the lambda list of a function. However, you can destructure a list from within a function via DESTRUCTURING-BIND.

? (destructuring-bind ((a &key b) (c (d e) &optional f))
                      '((1 :b 2) (3 (4 5) 6))
    (list a b c d e f))
(1 2 3 4 5 6)

Contents | Cover
Chapter 20 | Chapter 21 | Chapter 22

Copyright © 1995-2001, David B. Lamkins
All Rights Reserved Worldwide

This book may not be reproduced without the written consent of its author. Online distribution is restricted to the author's site.