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.
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 ? (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.
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 ? (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-2 ? (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 ? (optional-sample-1 1) (1 NIL NIL) ? (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 ? (optional-keyword-sample-1 1) (1 NIL NIL NIL NIL) ? (optional-keyword-sample-1 1 2) (1 2 NIL NIL NIL) ? (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 ? (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.
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 ? (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 ? (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 ? (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)) (loop (with-lock-grabbed (,items-lock) (setq ,work-item (pop ,items))) (when (null ,work-item) (return)) ;;(format t "~&running id ~D~%" ,pid) ,@body)))) (dotimes (i ,num-processes) ;;(format t "~&creating id ~D~%" ,id) (process-run-function (format nil "~A-~D" ,name i) #',process-fn i)))))) WITH-PROCESSES
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))) NIL 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)