避免无意中使用照应宏的陷阱

Avoiding the pitfall of using anaphoric macro unwittingly

我怎么知道我是否在呼叫 anaphoric macro?如果我在不知情的情况下这样做,一些看似未绑定的符号的行为可能与人们预期的完全不同。

示例

从列表中收集所有偶数很容易:

> (loop for i in '(1 2 3 4)        ;correct
       when (evenp i) collect i)
(2 4)

然而,如果有人想出给迭代变量命名的好主意 it(因为 "it" 似乎是 "item" 的一个很好的缩写;而且 C++ 人员经常迭代使用名为 it 的迭代器),结果突然大不相同:

> (loop for it in '(1 2 3 4)         ;wrong
       when (evenp it) collect it)
(T T)

这听起来可能有点做作,但最近发生了这样一个尴尬的错误,嗯,我认识的一个人。

那么如何避免再次陷入同样的​​陷阱此类错误?

这就是我不太喜欢照应宏的原因之一。

LOOP 宏使情况变得更糟,因为标识符不用作包的符号 - 而是仅按名称使用。示例:

其中一个可能位于无法直接访问符号 cl::it:

的用户包中
(cl:loop for it in '(1 2 3 4)
         when (cl:evenp it) collect it)  ; this is still problematic

因此,局部符号 it 仍将受到影响,因为照应变量 it 仍在遮蔽迭代变量 it。因此,将您自己的包用于符号 it.

无济于事

我不知道用户可以做什么 - 除了仔细阅读文档,照应变量肯定会 突出地 提到(?!? ):

CLHS 6.1.9 Notes about Loop

Use caution when using a variable named IT (in any package) in connection with loop, since it is a loop keyword that can be used in place of a form in certain contexts.

宏的开发人员可能想要检查用户是否使用照应变量的名称定义了一个变量 - 以相同的宏形式 - 并发出警告。变量也可能在宏之外定义 - 这仍然可能是问题的根源。

函数

函数可能会发生类似的事情:

(defmethod bar (a) (print (list :foo a)))

(defmethod bar :around (a)
  (flet ((call-next-method ()
           (print a)))
    (call-next-method)))

这里我们需要知道 DEFMETHOD 使局部函数 CALL-NEXT-METHOD 可用。如果我们不小心定义了一个具有相同名称的本地函数,那么我们将调用我们的版本 - 而不是使用 CLOS 版本...

这是照应宏未被一致喜欢并且人们通常尽量谨慎使用它们的一个主要原因。像 if-let 这样的显式绑定在实践中似乎更容易被接受。

然而,据我所知,LOOP 宏是规范中唯一提供隐式绑定的构造,但 NIL 块除外,如果您认为它们是相同的。此外,它有相当广泛的文档记录,不会很快改变。因此,给出的示例感觉有点人为。同时,不可否认的是,这种bug是有可能发生的。

So how to avoid this type of bugs?

也许您不需要做任何事情。错误时有发生,但这种错误不太可能经常发生。

但如果你愿意,你可以决定限制语言以禁止在 LOOP 中使用 it(因为你担心你或其他人会引入相同的错误):

(defpackage mycl (:use :cl) (:shadows #:loop))
(in-package mycl)

上面定义了 CL 的自定义方言,它隐藏了 loop 符号。 来自包 MYCL 的 可访问 (在没有给出包前缀时解析)的 loop 符号是来自 MYCL 的符号,而不是 CL:LOOP。 然后,您可以添加自己的支票:

(defmacro loop (&body body)
  (when (find "IT" body :test #'string=)
    (error "Forbidden IT keyword"))
  `(cl:loop ,@body))

这个定义应该足够了(它可能会漏掉一些情况)。 然后,您选择在您的项目中使用此包而不是 CL,因此,以下操作失败并出现错误:

(defun test ()
  (loop
    for it in '(1 2 3 4)
    when (evenp it) collect it))

...
  error: 
    during macroexpansion of
    (LOOP
      FOR
      IT
      ...).
    Use *BREAK-ON-SIGNALS* to intercept.

     Forbidden IT keyword

Compilation failed.

另一种检查方法如下(通过查看所有以 LOOP 为根的树会更严格,因此即使在其他有效情况下也可能出错):

(defmacro loop (&body body)
  (unless (tree-equal body (subst nil
                                  "IT"
                                  body
                                  :test #'string=
                                  :key (lambda (u)
                                         (typecase u
                                           ((or symbol string) (string u))
                                           (t "_")))))
    (error "Forbidden IT keyword"))
  `(cl:loop ,@body))

您可以对您发现有问题的其他构造应用相同的方法,但请注意,通常照应宏是由外部系统带来的,这是有意为之的,因此不应令人感到意外。但是即使你不知道你的一些宏是照应的,它们的文档甚至它们的命名约定应该足以防止错误(anaphora 系统引入以 [=18 开头的符号=],如 aifawhens,如 scase)。 如果您在交互式环境中工作(例如 Emacs/Slime,但其他环境也是如此),则可以轻松显示附加到函数或宏的文档。