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))

宏的优势仅仅是它在运行时对于在编译时扩展的任何调用更有效吗?还是它是一种范式?还是这本书只是试图为在宏中练习不同模式提供借口?

4

2 回答 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-environmentembed-it可以是宏或特殊操作符,确保某些代码在我们提供的嵌入式表单之前周围之后运行。

我们将提供的表单之前、前后和之后执行代码。那有用吗?有可能。它可能对更广泛的测试框架有用。

如果这听起来很熟悉,那么您会看到使用 CLOS 方法(主要、之前、之后、周围)可以获得类似的代码结构。测试将在主要方法中运行,其他代码将作为aroundbeforeafter方法运行。

请注意,宏也可以打印(参见 Hans23 的评论)、检查和/或更改提供的表单。

于 2016-01-27T10:03:00.013 回答
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. 此外,宏是可能出现在单元测试框架中的东西的起点。

于 2016-01-27T05:52:39.407 回答