Time and again I find myself in the situation that a function A needs to call function B with or without keyword arguments, depending on whether such keyword arguments have been given to function A.
Maybe a silly MWE is easier to follow: let's ask a boy called Tommy how his day at school was:
(defun answer (&key (math "boring" math-tested?)
(physics "heavy" physics-tested?))
(if math-tested?
(if physics-tested?
(format t "I got an ~A from math and a ~A from physics!" math physics)
(format t "I got an ~A from math." math))
(if physics-tested?
(format t "I got an ~A from physics." physics)
(format t "Math was ~A and physics was ~A." math physics))))
(defun father-son-talk (&key (math nil math-tested?)
(physics nil physics-tested?))
(format t "Hello Tommy, how was school today?~%")
(cond ((and math-tested? physics-tested?)
(answer :math math :physics physics))
(math-tested?
(answer :math math))
(physics-tested?
(answer :physics physics))
(t (answer))))
This works as intended but it is irksome for several reasons:
The calls to answer
are basically repeated. If answer
had several normal arguments in addition to the keyword ones, I have to be extra careful to do the same in all four cases. Worse yet when maintaining such a thing.
The complexity grows exponentially with the number of keyword arguments. What if the father would ask about English, geology and Tommy's lunch as well?
Further confusion can easily arise if the father and son have different default arguments (one might be tempted to write (answer :math math :physics physics)
).
Question: Assuming that answer
is a part of the interface I have to conform to, how can I simplify father-son-talk
? Ideally, I would like something like
(defun father-son-talk (&key (math nil math-tested?)
(physics nil physics-tested?))
(format t "Hello Tommy, how was school today?~%")
(answer :math (math math-tested?) :physics (physics physics-tested?)))
A common solution to this is apply
:
(defun father-son-talk (&rest all-options &key math physics)
(declare (ignore math physics))
(format t "Hello Tommy, how was school today?~%")
(apply #'answer all-options))
and if father-son-talk
doesn't need the keyword arguments itself at all, you can simplify this even more
(defun father-son-talk (&rest all-options)
(format t "Hello Tommy, how was school today?~%")
(apply #'answer all-options))
A nice trick you can do with functions like this is to say that you do want keyword arguments, and also you accept any keyword arguments, and then pass them on to implementation functions:
(defun father-son-talk (&rest all-options &key &allow-other-keys)
(format t "Hello Tommy, how was school today?~%")
(apply #'answer all-options))
This is like the previous version except that, for instance (father-son-talk 1)
is now an error: the arguments to it must all be keyword arguments. Which you want depends on whether answer
might expect non-keyword arguments, so both can be interesting.
There is also a nice case like this where a function itself wants keyword arguments but wants the set to be extensible: the single &rest
argument the function gets is in fact a property list, so you can do things like this:
(defun foo (&rest plist &key &allow-other-keys)
...
(getf ':x plist 1) ...)