在 Common Lisp 中同时使用 &rest 和 &key

use of &rest and &key at the same time in Common Lisp

我想同时使用 &rest&key。但是,下面的尝试代码:

(defun test (&rest args &key (name "who")) nil)
(test 1 2 3 4 5 :name "hoge")

导致错误:

*** - TEST: keyword arguments in (1 2 3 4 5 :NAME "hoge") should occur pairwise

并且当我只提供像 (test :name "hoge") 这样的关键字参数时,它起作用了。是否可以同时使用 &rest 和 &key?

在 Common Lisp 的函数定义中混合剩余参数和关键字参数通常不是一个好主意。如果这样做,您可能应该考虑重写函数定义,因为它可能会导致一些意外行为。如果 &rest 和 &key 都出现在参数列表中,那么这两种情况都会发生——所有剩余的值,包括关键字本身,都被收集到一个列表中,该列表绑定到 &rest 参数,适当的值也绑定到 &key参数。因此 (name "who") 关键字参数默认绑定到您的其余参数列表。如果您尝试输入参数 (1 2 3 4 5),您将收到错误消息,因为它们未绑定到您的参数 (name "who")。这是一个例子:

(defun test (&rest args &key (name "who"))
   (list args name))

这里有您的函数定义。如果我们尝试调用 return 参数列表的函数,我们将看到 &rest 参数绑定到它们的 &key 参数:

CL-USER> (test :name "Davis")
((:NAME "Davis") "Davis")

通过在同一个参数列表中混合 &rest 参数和关键字参数,您将无法输入任何与您的关键字参数不匹配的 rest 参数,这就是您在此处进入 breakloop 的原因。

现在,如果要创建宏,技术上可以在定义中使用多个参数列表,并在一个列表中添加关键字参数,在另一个列表中添加 &rest(或 &body)参数:

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

这里有一个示例,说明您可以如何做您想做的事。这相当 simple-minded,但它允许您定义接受任意数量参数以及零个或多个关键字参数的函数。然后有一个小蹦床,它从参数中提取关键字和它们的值并适当地调用函数。

这不是 production-quality 代码:让 trampoline-making 函数确切地知道它正在寻找的关键字,例如,这可能是已知的,而不是只是 '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)))

所以如果我现在定义这样一个函数:

(defun/rest/kw foo (args &key (x 1 xp))
  (declare (optimize debug))
  (values args x xp))

那我可以这样称呼它:

> (foo 1 2 3)
(1 2 3)
1
t

> (foo 1 2 :x 4 3)
(1 2 :x 4 3)
4
t

请注意 defun/rest/kw 可能不会做与 defun 相同的事情:特别是我认为它足以正确定义函数(而不是在编译时定义它)但是编译器可能没有意识到该函数在编译时存在(因此可能会出现警告),并且它也不会执行任何 implementation-specific 魔法。

&key&rest 实际上 非常 在 Common Lisp 中很常见,但几乎总是与 &allow-other-keys.

例如,假设您想为 write但不想列出 显式地使用所有关键字参数:

(defun my-write (object &rest args &key stream &allow-other-keys)
  (write "my wrapper" :stream stream)
  (apply #'write object args))

你会发现很多地方都有这个 &rest/&key/&allow-other-keys CLOS 实际所在的任何地方都使用模式 implemented.

不支持您尝试执行的操作。在 Common Lisp 中,可变参数函数的尾随“rest”参数与关键字参数一致。也就是说,当一个函数有关键字参数时,意味着函数的尾部参数(那些跟在固定参数和可选参数之后的参数)被解析为关键字参数

当你指定同时有&key&rest参数时,这意味着尾随参数被捕获为一个列表,这些参数也被解析为关键字参数。

我要说的是,关键字参数并不是从尾随参数中某个未指定的位置开始的。

如果我们想打电话

(test 1 2 3 4 5 :NAME "hoge") 

要工作,那么他的函数必须有五个固定位置参数(必需参数和可选参数的某种组合加起来最多五个)。那么尾随参数是:name"hoge"。如果存在 &rest 参数,则它会捕获列表 (:name "hoge").

如果其余列表是(1 2 3 4 5 ...)并且关键字参数从第一个关键字开始,您有自己的代码。

最明显的方法:

(defun test (&rest args)
  (let* ((keys (member-if #'keywordp args))
         (nonkeys (ldiff args keys)))
    (destructuring-bind (&key name) keys
      (list nonkeys name))))

测试:

[1]> (test 1 2 3 4 5 :name "hoge")
((1 2 3 4 5) "hoge")