【问题标题】:Avoiding the pitfall of using anaphoric macro unwittingly避免无意中使用照应宏的陷阱
【发布时间】:2018-07-31 19:47:44
【问题描述】:

我如何知道我是否拨打了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)

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

那么如何避免再次陷入同样的​​陷阱这种类型的bug呢?

【问题讨论】:

  • (T T) -- Lexical Scope Fairy 哭泣。

标签: scope macros common-lisp naming-conventions


【解决方案1】:

这是照应宏未得到一致喜欢的主要原因,人们通常会尽量谨慎地使用它们。像if-let 这样的显式绑定在实践中似乎更受欢迎。

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

那么如何避免此类错误呢?

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

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

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

上面定义了一个自定义的 CL 方言,它隐藏了 loop 符号。 loop 符号是可访问(在没有给出包前缀时解决)来自包 MYCL 的符号来自 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))

您可以将相同的方法应用于您发现有问题的其他构造,但请注意,通常照应宏是由外部系统带来的,这是故意完成的,因此不应让人感到意外。但是即使你不知道你的一些宏是照应,它们的文档甚至它们的命名约定也应该足以防止错误(照应系统引入了以a开头的符号,比如@ 987654330@、awhens,如 scase)。 如果您在交互式环境(例如 Emacs/Slime,但也包括其他环境)中工作,则可以轻松地显示附加到函数或宏的文档。

【讨论】:

  • 感谢您的建议。实际上,从一位经验丰富的用户那里听说他们不知道任何进一步的此类示例是我所期待的答案。由于我不太可能很快忘记“它”,我可能会选择选项 1(即,不要做任何特别的事情)。但是,如果您发现任何其他示例,请告诉我。
  • 问题不在于照应宏,因为当您使用一个照应宏时,您会故意使用它来利用其it 符号。问题是loop 不是一个照应宏,它具有it 作为一个晦涩的功能,并且不需要对涉及it 的令人惊讶的阴影情况进行诊断。
【解决方案2】:

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

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

在将名为 IT 的变量(在任何包中)与循环关联使用时要小心,因为它是一个循环关键字,可以在某些上下文中代替表单。

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

函数

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

(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 版本...

【讨论】:

  • 谢谢你的链接,我应该检查一下。我不接受您的回答的唯一原因是我发现 coredump 更好。
  • @DominikMokriš : 这东西很难找到,不能指望有人记得它...
  • 为了进行体面的诊断,loop 宏会检查名为it 的变量是否绑定在loop 内部或存在于周围环境中。如果是这样,它必须对所有内部表单进行代码遍历,以确定它们对it 的引用是否来自它们中的任何一个。如果是这样,请警告可能存在的歧义。
  • 我认为重新绑定 cl:call-next-method 是被禁止的,但重点仍然在于用户定义的函数可以意外访问(顺便说一句,我认为没有标准中的cl::it 符号)
  • @coredump:重新绑定cl:call-next-method 的效果在标准中未定义。意思是:“后果可能从无害到致命”。是的,cl::it 不存在。
猜你喜欢
  • 2010-11-16
  • 2012-04-03
  • 2015-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-16
  • 2018-04-17
  • 2019-07-14
相关资源
最近更新 更多