这个宏有什么优势吗?

8

我正在阅读Peter Seibel的《Practical Common Lisp》。在第9章中,他带领读者创建了一个单元测试框架,并包含以下宏来确定列表是否仅由真表达式组成:

(defmacro combine-results (&body forms)
  (let ((result (gensym)))
    `(let ((,result t))
       ,@(loop for form in forms collect `(unless ,form (setf ,result nil)))
       ,result)))

我不清楚在这里使用宏的优势是什么,似乎以下内容更清晰,对于动态值也更有效率:

(defun combine-results (&rest expressions)
  (let ((result t))
    (loop for expression in expressions do (unless expression (setf result nil)))
    result))

宏的优势只是在于编译时展开调用时更加高效吗?还是有范式上的优势?或者这本书只是试图为练习宏中的不同模式提供借口?

2
宏的真正好处在于它可以访问原始形式,因此可以打印未能评估为真值的表达式。遗憾的是,这本书的示例实际上并没有展示这一点。 - hans23
1
@hans23: 在这本书的代码中,表单实际上是宏,可以打印结果。 - Rainer Joswig
2个回答

9
在这种情况下,也许并不重要,但对于未来的版本来说,使用宏可能更有用。使用宏是否有意义?这取决于使用情况:
使用函数
(combine-results (foo) (bar) (baz))

请注意,运行时Lisp会看到combine-results是一个函数。然后它会评估参数。然后使用结果值调用函数combine-results。这个评估规则在Common Lisp中是硬编码的。
这意味着:函数的代码在参数被评估之后才运行。
使用宏的方法:
(combine-results (foo) (bar) (baz))

Lisp识别到它是一个宏,便在宏展开时调用该宏并生成代码。生成的代码完全取决于宏的实现。这使我们能够生成如下代码:

(prepare-an-environment

  (embed-it (foo))
  (embed-it (bar))
  (embed-it (baz))

  (do-post-processing))

这段代码将被执行。例如,您可以设置系统变量、提供错误处理程序、设置一些报告机制等。每个单独的表格也可以嵌入到其他表格中。函数运行后,可以进行一些清理、报告等操作。`prepare-an-environment`和`embed-it`将是宏或特殊运算符,它们确保在提供的嵌入式表格之前、期间和之后运行某些代码。
我们将有代码在提供的表格之前、期间和之后执行。这有用吗?可能会。对于更广泛的测试框架可能很有用。
如果这听起来很熟悉,那么您会发现可以使用CLOS方法(primary、before、after、around)获得类似的代码结构。测试将在主要方法中运行,而其他代码将作为around、before和after方法运行。
请注意,宏还可以打印(请参见Hans23的评论)、检查和/或更改所提供的表格。

7

您的观察基本上是正确的;实际上您的函数可以简化为:

(defun combine-results (&rest expressions)
  (every #'identity expressions))  ;; i.e. all expressions are true?

由于宏无条件从左到右评估其所有参数,并在所有参数都为真时产生T,因此它基本上只是内联优化可以通过函数完成的事情。可以使用(declaim 'inline ...)请求将函数内联。此外,我们可以使用define-compiler-macro编写编译器宏来处理函数。使用该宏,我们可以生成扩展代码,并将其作为可以apply和间接调用的函数。
函数内部计算结果的其他方法:
(not (position nil expressions))
(not (member nil expressions))

这个例子看起来像宏练习:创建一个gensym,并使用loop生成代码。此外,该宏是可能出现在单元测试框架中的起点。


1
如果宏在评估其参数时中途停止,例如用于优化,那么它可能很有用,而函数方法永远不允许这样做。但是以目前的形式,它并没有什么用处。 - joao
2
@JoaoTavora 如果宏短路,那么它将是标准and宏的多余实现! - Kaz
@Kaz,不完全是这样,因为如果所有参数都非nil,则会返回“t”。但基本上是这样。我认为在这种情况下真正的用途是@hans23的“冗长”角度:通过使用宏,您可以打印表单并对表单本身执行任何操作。 - joao
@JoaTavora;是的,这相当于在AND的参数中再添加一个T项。严格返回T表示为真的布尔表达式具有优点:它们可以可靠地与EQ进行比较。(defmacro bool-and (&rest args) \(and ,*args t))`。 - Kaz
1
@JoeTavora:最近我意识到这一点,当我将Lisp方言中的库函数chr-isdigit更改为在字符是数字时返回数字值而不是t(使该函数类似于CL中的digit-char-p函数)时。 然而,这破坏了像[partition-by digit-char-p sequence]这样的代码,因为现在两个不同的连续数字构成不同的分区,因为结果不是“相等的”。 - Kaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接