use of &rest and &key at the same time in Common Lisp
The combination of &key
and
&rest
are actually very
common in Common Lisp, but almost always together with
&allow-other-keys
.
E.g., suppose you want to define a wrapper for
write
but do not want to list
explicitly all the keyword arguments it takes:
(defun my-write (object &rest args &key stream &allow-other-keys)
(write "my wrapper" :stream stream)
(apply #'write object args))
You will find many places where this
&rest
/&key
/&allow-other-keys
patterns is used wherever the CLOS is actually
implemented.
It's generally not a good idea to mix rest parameters with keyword parameters within a function definition in Common Lisp. If you do so, you should probably consider rewriting the function definition because it can lead to some unexpected behavior. If both &rest and &key appear in a parameter list, then both things happen--all the remaining values, which include the keywords themselves, are gathered into a list that's bound to the &rest parameter, and the appropriate values are also bound to the &key parameters. So the (name "who") keyword parameter is bound to your list of rest parameters by default. if you try to enter the arguments (1 2 3 4 5), you will get an error because they aren't bound to your parameter (name "who"). Here is an example:
(defun test (&rest args &key (name "who"))
(list args name))
Here we have your function definition. If we try to call the function which return a list of the arguments, we will see that the &rest parameters are bound to they &key parameters here:
CL-USER> (test :name "Davis")
((:NAME "Davis") "Davis")
By mixing &rest parameters and keyword parameters in the same parameter list, you won't be able to enter any rest parameters that don't match your keyword parameter which is why you enter into a breakloop here.
Now, if you want to create a macro, you can technically use multiple parameter lists within the definition, and add keyword parameters in one list, and &rest (or &body) parameters in the other list:
(defmacro hack-test ((&key (name "who")) &body body)
`(list ,name ,@body))
CL-USER> (hack-test (:name "Ricky")
(+ 2 3))
("Ricky" 5)
CL-USER> (hack-test ()
(+ 2 4)
(+ 4 5)
(+ 9 9))
("who" 6 9 18)
CL-USER>
Here's an example of how you might do what you want to do. This is fairly simple-minded, but it allows you to define functions which take any number of arguments, together with zero or more keyword arguments. There is then a little trampoline which pulls keywords and their values out of the arguments and calls the function appropriately.
This is not meant to be production-quality code: it would clearly be better to have the trampoline-making function know exactly what keywords it was looking for for instance, which could be known, rather than just 'any keywords'.
(defun make-kw-trampoline (fn)
;; Given a function which takes a single rest arg and a bunch of
;; keyword args, return a function which will extract the keywords
;; from a big rest list and call it appropriately
(lambda (&rest args)
(loop for (arg . rest) on args
if (keywordp arg)
if (not (null rest))
collect arg into kws and collect (first rest) into kws
else do (error "Unpaired keyword ~S" arg)
finally (return (apply fn args kws)))))
(defmacro defun/rest/kw (name (rest-arg and-key . kw-specs) &body decls-and-forms)
;; Define a function which can take any number of arguments and zero
;; or more keyword arguments.
(unless (eql and-key '&key)
(error "um"))
(multiple-value-bind (decls forms) (loop for (thing . rest) on decls-and-forms
while (and (consp thing)
(eql (first thing) 'declare))
collect thing into decls
finally (return
(values decls (cons thing rest))))
`(progn
(setf (fdefinition ',name)
(make-kw-trampoline (lambda (,rest-arg &key ,@kw-specs)
,@decls
(block ,name
,@forms))))
',name)))
So if I now define a function like this:
(defun/rest/kw foo (args &key (x 1 xp))
(declare (optimize debug))
(values args x xp))
Then I can call it so:
> (foo 1 2 3)
(1 2 3)
1
t
> (foo 1 2 :x 4 3)
(1 2 :x 4 3)
4
t
Note that defun/rest/kw
may not do the same thing that defun
does: in particular I think it does enough to define the function properly (and not to define it at compile time) but the compiler may not realise the function exists at compile time (so there may be warnings), and it also does not do any implementation-specific magic.