Common Lisp:如果关键字参数已传递给我,如何传递它

Common Lisp: How to pass a keyword argument iff it has been passed to me

我一次又一次地发现自己处于函数 A 需要使用或不使用关键字参数调用函数 B 的情况,这取决于是否已将此类关键字参数提供给函数 A。

也许一个愚蠢的 MWE 更容易理解:让我们问问一个叫 Tommy 的男孩他在学校过得怎么样:

(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))))

这按预期工作,但由于以下几个原因令人厌烦:

  1. answer的调用基本都是重复的。如果 answer 除了关键字参数之外还有几个普通参数,我必须格外小心,在所有四种情况下都做同样的事情。更糟糕的是,当维护这样的东西时。

  2. 复杂度随着关键字参数的数量呈指数增长。如果爸爸问英语、地质学和汤米的午餐怎么办?

  3. 如果父子具有不同的默认参数,则很容易引起进一步的混淆(可能会想写 (answer :math math :physics physics))。

问题:假设answer是我必须遵守的接口的一部分,我如何简化father-son-talk?理想情况下,我想要

(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?)))

一个常见的解决方案是 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))

如果 father-son-talk 根本不需要关键字参数本身,您可以进一步简化它

(defun father-son-talk (&rest all-options)
  (format t "Hello Tommy, how was school today?~%")
  (apply #'answer all-options))

你可以用这样的函数做一个很好的技巧,就是说你确实想要关键字参数,并且你也接受 any 关键字参数,然后将它们传递给实现函数:

(defun father-son-talk (&rest all-options &key &allow-other-keys)
  (format t "Hello Tommy, how was school today?~%")
  (apply #'answer all-options))

这与以前的版本类似,除了 (father-son-talk 1) 现在是一个错误:它的参数必须全部是关键字参数。您想要哪个取决于 answer 是否期望 non-keyword 个参数,因此两者都很有趣。

还有这样一个很好的例子,函数本身需要关键字参数但希望集合是可扩展的:函数获得的单个 &rest 参数实际上是一个 属性 列表,所以你可以这样做:

(defun foo (&rest plist &key &allow-other-keys)
  ...
  (getf ':x plist 1) ...)