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

问题描述

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

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

(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是否可能需要非关键字参数,因此两者都可能很有趣。

还有一个很好的例子,一个函数本身想要关键字参数,但希望集合是可扩展的:该函数获取的单个&rest参数实际上是一个属性列表,因此您可以执行以下操作这个:

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