Common Lisp 中 &rest 和 &key 参数的 Lambda 列表错误

Lambda List Error with &rest and &key arguments in Common Lisp

下面的函数旨在用几个参数创建一个符号。但是,调用它会产生关键字错误。

(defun create-symbol (&rest objects &key intern (package *package*))
  "Creates a symbol from the objects."
  (let ((arg-string (format nil "~{~A~^~}" (first objects))))
    (if intern
        (values (intern arg-string package))
      (make-symbol arg-string))))

例如,(create-symbol "A" 1) 生成 Unknown &KEY argument: "A" 而不是 #:A1

如果没有提交关键字,也不确定 (first objects) 是否是访问 &rest 参数的正确方法。

感谢您帮助验证此函数的预期操作。

编辑:根据下面的评论,当参数是 lambda 列表关键字 &optional、&rest 和 &key 的组合时,手动解析它们似乎是一种可行的方法。下面的函数似乎完成了我最初的意图:

(defun create-symbol (&rest objects&keys)
  "Creates a symbol from the objects,
   with optional keywords :intern and :package."
  (let* ((keys (member-if #'keywordp objects&keys))
         (objects (ldiff objects&keys keys))
         (arg-string (format nil "~{~A~^~}" objects)))
    (if (getf keys :intern)
      (intern arg-string (or (getf keys :package) *package*))
      (make-symbol arg-string))))

基本上你不能这样做。使 intern 不是关键字参数或进行您自己的关键字解析。下面是普通函数的参数解析规则:

  1. 一个函数有三种用于解析的参数:必需的、可选的和剩余的
  2. 任何出现在 lambda 列表关键字(例如 &optional)之前的参数都是必需的参数。他们必须通过。对于进一步的步骤,只计算在所需参数之后传递的参数。
  3. 在 lambda 列表中的必需参数之后可能会出现 &optional,然后是可选参数。如果有任何传递的参数尚未解析,则这些参数将被视为可选参数,直到没有可选参数需要解析为止。如果没有参数需要解析,那么我们就完成了。
  4. 在可选参数(如果有的话)之后可以有一个剩余参数(&rest 后跟一个要绑定的符号),关键字参数(前缀为 &key&allow-other-keys修改解析的关键字。任何在此阶段尚未解析的传递参数称为剩余参数。这是它们解析的方式:
    1. 如果 lambda 列表中有一个 &rest 参数,将其绑定到任何尚未解析的参数。
    2. 如果有任何关键字参数,则要求其余参数的数量为偶数,并且对于每个剩余参数,首先读取一个键并将其与未绑定关键字参数的符号匹配。将该参数绑定到下一个剩余参数。如果键重复,则为错误。如果一个键是未知的,除非指定了 &allow-other-keys 关键字,否则它是一个错误。

可以想象以下转换:

(defun f ( { args } [ &rest rest ] &key { kwargs } [ &allow-other-keys ] )
  ...)

;; - - ->

(defun f ( { args } &rest rest )
  (destructuring-bind ( &key { kwargs } [ &allow-other-keys ] ) rest
    ...))

这可能会更清楚一些。


你可以让你的函数按照你想要的方式运行(不使用 getf 虽然因为奇偶校验)但我认为这是错误的。考虑以下因素:

(defun foobar-sym (k)
  (create-symbol 'foo k 'bar))

CL-USER> (foobar-sym :baz)
#:FOOBAZBAR
CL-USER> (foobar-sym :intern)
FOO

这有点奇怪。

显示问题所在的一个非常小的例子是:

(defun test-key-rest (&rest args &key a (b t))
    (list 'args args 'a a 'b b))
(test-key-rest :a 1 :b 2); => (ARGS (:A 1 :B 2) A 1 B 2)
(test-key-rest :a 1 :b 2 "rest now?");;; Error: The passed key "rest now?" is not defined for this function
(test-key-rest :a 1 :b 2 :c 3);;; Error: The passed key :C is not defined for this function

也许可以使用 &allow-other-keys, but I think it would be messy. I remembered reading about this kind of situation in Practical Common Lisp,其中 Peter Seibel 写道(强调我的):

The other two combinations, either &optional or &rest parameters combined with &key parameters, can lead to somewhat surprising behavior.

我建议将两个参数列表分开。 destructuring-bind 让一切变得简单:

(defun test-two-arg-lists (keys &rest args)
    (destructuring-bind (&key (a nil) (b t)) keys
        (list 'a a 'b b 'args args)))
(test-two-arg-lists (list :a 1) "more" "please"); => (A 1 B T ARGS ("more" "please"))

但是我(我假设其他人)不想构建第一个关键字参数列表,所以让我们让它按照我们期望的宏来评估它的参数:

(defmacro test-two-nice (keys &rest args)
    `(test-two-arg-lists (list ,@keys) ,@args))
(test-two-nice (:a 1) "more" "please"); => (A 1 B T ARGS ("more" "please"))

所以把它们放在一起:

(defun create-symbol-fn (keys &rest objects)
  "Creates a symbol from the objects."
  (destructuring-bind (&key intern (package *package*)) keys
    (let ((arg-string (format nil "~{~A~}" objects)))
      (if intern
          (values (intern arg-string package))
        (make-symbol arg-string)))))
(defmacro create-symbol (keys &rest objects)
  `(create-symbol-fn (list ,@keys) ,@objects))
(create-symbol (:intern nil) 'a 'b 'c 'd); => #:ABCD