何时使用宏或不使用宏


12

什么时候应该在程序中使用宏?

这个问题的灵感来自@tarsius 的翔实答案。我相信许多人都有分享其他人的丰富经验。我希望这个问题成为伟大的主观问题之一,并为Emacs程序员提供帮助。

Answers:


13

经验法则

将宏用于语法,但不要用于语义。

将宏用于某些语法糖是可以的,但是将逻辑放入宏主体中而不管它是在宏本身中还是在扩展中都是一个坏主意。如果您需要这样做,请改写一个函数,并为语法糖编写一个“伴侣宏”。简而言之,如果您let的宏中有一个,则表示您正在寻找麻烦。

为什么?

宏有很多缺点:

  • 它们在编译时会扩展,因此必须仔细编写以维护多个版本之间的二进制兼容性。例如,删除宏扩展中使用的函数或变量将破坏所有正在使用此宏的相关程序包的字节码。换句话说,如果以不兼容的方式更改宏的实现,则必须重新编译所有从属程序包。二进制兼容性并不总是很明显……
  • 您必须注意保持直观的评估顺序。编写过于频繁,在错误的时间,在错误的位置等的形式来评估表单的宏很容易……
  • 您必须注意绑定语义:宏从扩展站点继承其绑定上下文,这意味着您不知道先验存在哪些词汇变量,甚至不知道词汇绑定是否可用。宏必须同时使用两种绑定语义。
  • 与此相关的是,在编写创建闭包的宏时必须当心。请记住,您是从扩展站点获取绑定,而不是从词法定义获取绑定,因此请注意关闭的内容以及创建闭包的方式(请记住,您不知道词法绑定在扩展站点是否处于活动状态,以及您甚至可以使用闭包)。

1
有关词法绑定与宏扩展的注解特别有趣。谢谢lunaryorn。(我从未为我编写的任何代码启用词法绑定功能,所以这不是我从未想到的冲突。)
phils

6

根据我的阅读,调试和修改我和其他人的Elisp代码的经验,我建议尽可能避免使用宏。

关于宏的显而易见的事情是它们不评估其参数。这意味着,如果您在宏中包装了一个复杂的表达式,则不知道是否会对它进行评估以及在哪种情况下进行评估,除非您查找宏定义。对于您自己而言,这并不是什么大不了的事情,因为您知道自己的宏的定义(当前,可能在几年后)。

此缺点不适用于函数:所有args均在函数调用之前进行求值。这意味着您可以在未知的调试情况下提高评估复杂度。您甚至可以直接跳过您不知道的函数的定义,只要它们返回简单的数据,并且您假定它们不触及全局状态即可。

用另一种方式改写它,宏成为语言的一部分。如果您正在阅读带有未知宏的代码,那么您几乎会以一种未知的语言来阅读代码。

上述保留的例外情况:

标准宏喜欢pushpopdolist为你做的真的很多,而且是众所周知的其他程序员。它们基本上是语言规范的一部分。但是实际上不可能标准化自己的宏。不仅很难像上面的示例那样提出简单有用的东西,而且更难以说服每个程序员学习它。

另外,我不介意像这样的宏save-selected-window:它们以相同的方式存储和恢复状态并评估其args progn。非常简单,实际上使代码更简单。

OK宏的另一个示例可以是defhydrause-package。这些宏基本上带有它们自己的迷你语言。而且他们仍然要么对待简单数据,要么表现得像progn。另外,它们通常位于顶层,因此调用堆栈上没有变量可以依赖宏参数,这使其变得更简单。

一个坏的宏观的一个例子,在我看来,是magit-section-actionmagit-section-case在Magit。第一个已被删除,但第二个仍然保留。


4

我会直言不讳:我不明白“用于语法而不是语义”的含义。这就是为什么该答案的一部分将处理lunaryon的答案,而另一部分将是我尝试回答原始问题的原因。

在编程中使用“语义”一词时,是指语言作者选择解释其语言的方式。这通常归结为一组公式,这些公式将语言的语法规则转换为假定读者熟悉的其他规则。例如,普洛特金(Plotkin)在他的《操作语义的结构化方法》中使用逻辑推导语言来解释抽象语法的计算结果(运行程序意味着什么)。换句话说,如果语法在某种程度上构成了编程语言,那么它就必须具有某种语义,否则就不是一种编程语言。

但是,让我们暂时忘记形式定义,毕竟,了解意图很重要。因此,当不向程序添加新含义的时候,lunaryon似乎会鼓励他的读者使用宏。对我来说,这听起来很奇怪,与实际使用宏的方式形成鲜明对比。以下是清楚地产生新含义的示例:

  1. defun 和朋友,这是语言的重要组成部分,无法用描述函数调用的相同术语来描述。

  2. setf描述函数工作原理的规则不足以描述类似表达式的效果(setf (aref x y) z)

  3. with-output-to-string还可以更改宏中代码的语义。同样,with-current-buffer还有一堆其他with-宏。

  4. dotimes 并且在功能上不能描述类似的内容。

  5. ignore-errors 更改其包装的代码的语义。

  6. cl-libpackage,eieiopackage和Emacs Lisp中使用的几乎其他无处不在的库中定义了一堆宏,尽管看起来函数形式有不同的解释。

因此,宏(如果有的话)是将新语义引入语言的工具。我想不出另一种方法(至少在Emacs Lisp中没有)。


当我不使用宏时:

  1. 当他们不引入任何具有新语义的新构造时(即该功能将同样出色地完成工作)。

  2. 这只是一种便利(例如,创建一个名为的宏mvb,它会扩展为cl-multiple-value-bind使其更短)。

  3. 当我期望宏将隐藏大量错误处理代码时(如前所述,宏很难调试)。


当我强烈希望宏胜于函数时:

  1. 当特定域的概念被函数调用遮盖时。例如,如果需要将相同的context参数传递给一堆需要连续调用的函数,我宁愿将它们包装在一个context隐藏参数的宏中。

  2. 当函数形式的通用代码过于冗长时(Emacs Lisp中的裸迭代结构太冗长,以至于我不喜欢,并不必要地使程序员暴露于底层实现细节)。


插图

下面是经过简化的版本,旨在使人们对计算机科学中语义的含义有一个直观的认识。

假设您有一种非常简单的编程语言,仅包含以下语法规则:

variable := 1 | 0
operation := +
expression := variable | (expression operation expression)

这种语言我们可以构造“程序”之类(1+0)+10((0+0))

但是,只要我们不提供语义规则,这些“程序”就毫无意义。

假设我们现在为我们的语言配备(语义)规则:

0+0  0+1  1+0  1+1  (exp)
---, ---, ---, ---, -----
 0   1+0   1    0    exp

现在我们可以实际使用这种语言进行计算了。也就是说,我们可以采用语法表示形式,抽象地理解它(换句话说,将其转换为抽象语法),然后使用语义规则来操纵该表示形式。

当我们谈论Lisp语言家族时,函数评估的基本语义规则大致是lambda微积分的规则:

(f x)  (lambda x y)   x
-----, ------------, ---
 fx        ^x.y       x

引用和宏扩展机制也被称为元编程工具,即它们允许一个谈论有关,而不是仅仅编程程序。他们通过使用“元层”创建新的语义来实现此目的。这样的简单宏的示例是:

(a . b)
-------
(cons a b)

这实际上不是Emacs Lisp中的宏,但它本来可以。我选择只是为了简单起见。注意,上面定义的语义规则都不适用于这种情况,因为最接近的候选者(f x)将解释f为一个函数,而a不一定是一个函数。


1
“语法糖”在计算机科学中并不是真正的术语。工程师使用它来表示各种含义。所以我没有确切的答案。由于我们正在讨论语义,因此,解释形式的语法的规则:(x y)大致是“评估y,然后用先前评估的结果调用x”。当您编写时(defun x y),不会评估y,也不会评估x。是否可以使用不同的语义编写等效效果的代码,并不否认这段具有自己语义的代码。
wvxvw

1
关于您的第二条评论:您能否请我参考您所使用的宏的定义,其中指出了您的要求?我只是给您提供了一个示例,其中通过宏引入了新的语义规则。至少在CS的精确意义上。我认为关于定义的争论并不富有成效,我也认为CS中使用的定义最适合本次讨论。
wvxvw

1
好吧……您碰巧对“语义学”一词有什么想法,这很讽刺,如果您考虑一下:)但是,实际上,这些东西确实有非常精确的定义,您就是这样。 。忽略那些。我在答案中链接的这本书是CS相应课程中教授的关于语义的标准教科书。如果您只阅读了Wiki的相应文章:en.wikipedia.org/wiki/Semantics_%28computer_science%29,您将看到它的实际含义。该示例是解释(defun x y)与功能应用程序的规则。
wvxvw

哦,我认为这不是我进行讨论的方式。尝试甚至开始尝试都是错误的。我删除了我的评论,因为所有这些都没有意义。很抱歉浪费您的时间。
lunaryorn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.