lisp:何时使用函数与宏

lis*_*ons 5 lisp macros common-lisp

在我不断学习 lisp 的过程中,我遇到了一个概念问题。这有点类似于这里的问题,但也许在主题上适合说我的问题是一个抽象级别。

通常,什么时候应该创建宏而不是函数?在我看来,也许天真地,很少有情况需要创建宏而不是函数,并且在大多数剩余情况下,函数通常就足够了。在这些其余情况中,宏的主要附加价值似乎是语法的清晰度。如果是这样的话,那么对于个体程序员来说,似乎不仅是选择使用宏的决定,而且其结构的设计也可能从根本上是特殊的。

这是错误的吗?是否有一般情况概述何时使用宏而不是函数?我说的对吗,语言需要宏的情况通常很少?最后,是否存在宏所期望的通用语法形式,或者它们通常被程序员用作简写?

lis*_*ons 5

我从 Paul Graham 的On Lisp中找到了详细的答案,并添加了粗体强调:

\n\n
\n\n

宏可以做函数可以\xe2\x80\x99t 做的两件事:它们可以控制(或阻止)对其参数的求值,并且它们可以直接扩展到调用上下文中。任何需要宏的应用程序最终都需要这些属性中的一个或两个。

\n\n

...

\n\n

宏通过四种主要方式使用此控件:

\n\n
    \n
  1. 转型。Common Lispsetf宏是一类在求值之前将参数分开的宏之一。内置访问函数通常会具有相反的功能,其目的是设置访问函数检索的内容。的逆序carrplacacdrrplacd、 等等。我们可以setf使用对此类访问函数的调用,就好像它们是要设置的变量一样,如 中(setf (car x) \xe2\x80\x99a),它可以扩展为(progn (rplaca x \xe2\x80\x99a) \xe2\x80\x99a)。 \n要执行此技巧,setf必须查看其第一个参数的内部。要知道上面的情况需要rplacasetf必须能够看到第一个参数是一个以 开头的表达式car。因此setf,以及任何其他转换其参数的运算符,都必须编写为宏。

  2. \n
  3. 捆绑。词法变量必须直接出现在源代码中。setq例如,的第一个参数不会被求值,因此构建的任何内容都setq必须是扩展为 a 的宏setq,而不是调用它的函数。同样,对于像 这样的运算符let,其参数将作为 lambda 表达式中的参数出现,对于像这样do扩展为lets 的宏,等等。任何要改变其参数的词法绑定的新运算符都必须编写为宏。

  4. \n
  5. 有条件评价。函数的所有参数都会被评估。在类似的构造中when,我们希望某些参数仅在某些条件下才被计算。这种灵活性只有通过宏才能实现。

  6. \n
  7. 多重评价。函数的参数不仅全部被求值,而且全部被求值一次。我们需要一个宏来定义类似 的构造do,其中某些参数将被重复求值。

  8. \n
\n\n

还有多种方法可以利用宏的内联扩展。需要强调的是,扩展出现在宏调用的词法上下文中,因为宏的三种用途中的两种依赖于这一事实。他们是:

\n\n
    \n
  1. 使用调用环境。宏可以生成包含变量的扩展,该变量的绑定来自宏调用的上下文。以下宏的行为:\n (defmacro foo (x) \xe2\x80\x98(+ ,x y))\n取决于调用 foo 时 y 的绑定。\n这种词汇交流通常更多地被视为传染源,而不是快乐源。通常编写这样的宏是不好的风格。函数式编程的理想也适用于宏:与宏进行通信的首选方式是通过其参数。事实上,很少需要使用调用环境,因此大多数情况下,它都是错误地发生的......

  2. \n
  3. 包裹着新的环境。宏还可以使其参数在新的词法环境中求值。典型的例子是let,它可以作为lambda. 在像 ,这样的表达式体内(let ((y 2)) (+ x y))y将引用一个新变量。

  4. \n
  5. 保存函数调用。宏扩展内联插入的第三个结果是,在编译的代码中,没有与宏调用相关的开销。到运行时,宏调用已被其扩展所取代。(内联声明的函数原则上也是如此。)

  6. \n
\n\n

...

\n\n

那些可以以任何方式编写的运算符(即作为函数或宏)怎么样?...当我们面临这样的选择时,需要考虑以下几点:

\n\n

优点

\n\n
    \n
  1. 编译时计算。宏调用涉及两次计算:扩展宏时以及计算扩展时。Lisp 程序中的所有宏扩展都是在编译程序时完成的,并且在编译时可以完成的每一位计算都是一位不会在\xe2\x80\x99 时减慢程序速度的位\xe2\x80 \x99s 正在运行。如果可以编写一个运算符来在宏扩展阶段完成一些工作,那么将其设为宏会更有效,因为无论智能编译器可以\xe2\x80\x99t自己完成什么工作,函数都必须完成在运行时。avg第 13 章描述了在扩展阶段执行一些工作的宏。

  2. \n
  3. 与 Lisp 集成。有时,使用宏而不是函数将使程序与 Lisp 的集成更加紧密。你也许可以使用宏将问题转化为 Lisp 已经知道如何解决的问题,而不是编写程序来解决某个问题。如果可能的话,这种方法通常会让程序变得更小、更高效:更小是因为 Lisp 正在为你做一些工作,而更高效是因为生产 Lisp 系统通常比用户程序有更多的工作。这种优势主要出现在嵌入式语言中,从第 19 章开始描述。

  4. \n
  5. 保存函数调用。宏调用直接扩展到它出现的代码中。因此,如果您将一些经常使用的代码编写为宏,则可以在每次使用\xe2\x80\x99时保存一个函数调用。在早期的 Lisp 方言中,程序员利用宏的这一特性来保存运行时的函数调用。在 Common Lisp 中,这项工作应该由声明为内联的函数接管。\n通过将函数声明为内联,您要求将其直接编译到调用代码中,就像宏一样。然而,理论与实践之间存在着差距;CLTL2(第 229 页)表示 \xe2\x80\x9ca 编译器可以自由地忽略此声明,\xe2\x80\x9d 和一些 Common Lisp 编译器会这样做。如果您被迫使用这样的编译器,那么使用宏来保存函数调用可能仍然是合理的......

  6. \n
\n\n

缺点

\n\n
    \n
  1. 函数是数据,而宏更像是编译器的指令。函数可以作为参数传递(例如,传递给apply)、由函数返回或存储在数据结构中。这些事情对于宏来说都是不可能的。\n在某些情况下,您可以通过将宏调用包含在 lambda 表达式中来获得所需的结果。例如,如果您想要applyfuncall某些宏: ,则此方法有效> (funcall #\xe2\x80\x99(lambda (x y) (avg x y)) 1 3) --> 2。然而,这是一个不便之处。它并不总是有效:即使像 一样avg,宏有一个&rest参数,也没有办法向它传递不同数量的参数。

  2. \n
  3. 源代码的清晰度。宏定义可能比等效的函数定义更难阅读。因此,如果将某些内容编写为宏只会使程序稍微好一点,那么使用函数可能会更好。

  4. \n
  5. 运行时清晰度。宏有时比函数更难调试。如果您在包含大量宏调用的代码中遇到运行时错误,则您在回溯中看到的代码可能包含所有这些宏调用的扩展,并且可能与您最初编写的代码几乎没有相似之处。\n并且因为宏扩展时消失,它们在运行时不负责。您通常可以使用\xe2\x80\x99ttrace来查看宏是如何被调用的。如果它有效,trace将显示对宏\xe2\x80\x99s 扩展器函数的调用,而不是宏调用本身。

  6. \n
  7. 递归。 在宏中使用递归并不像在函数中那么简单。尽管宏的扩展函数可能是递归的,但扩展本身可能不是。第 10.4 节讨论宏中递归的主题......

  8. \n
\n\n

考虑完宏可以做什么之后,下一个要问的问题是:我们可以在哪些类型的应用程序中使用它们?与宏使用的一般描述最接近的是,它们主要用于语法转换。这并不是说宏的范围受到限制。由于 Lisp 程序是由列表(即 Lisp 数据结构)组成的,因此 \xe2\x80\x9c 句法转换 \xe2\x80\x9d 确实可以走很长一段路......

\n\n

宏应用程序在小型通用宏(如 while)和后面章节中定义的大型专用宏之间形成了连续体。一方面是实用程序,类似于每个 Lisp 内置的宏。它们通常很小、很笼统并且是单独编写的。但是,您也可以为特定类别的程序编写实用程序,并且当您拥有一组可在图形程序中使用的宏时,它们开始看起来像图形编程语言。在连续体的远端,宏允许您用与 Lisp 明显不同的语言编写整个程序。以这种方式使用的宏据说可以实现嵌入式语言。

\n