7

在将 lisp 代码文件编译为字节码或原始程序集(或 fasl 文件)时,我在理解读取宏的内容时遇到了一些麻烦。或者也许我确实明白但不知道。我真的很困惑。

当您使用读取宏时,您不必提供可用的源吗?

如果这样做,那么您必须执行构成读取宏功能的源代码。如果您不这样做,那么当您可以做类似的事情时,它们如何工作read-char

要做到这一点,如果你想让 read 宏使用预先定义的变量,你必须执行它之前的所有代码,所以这变成了运行时,一切都搞砸了。

如果你不运行它之前的代码,那么上面定义的东西将不可用。

定义读取宏的函数或编译器宏呢?我会假设它们根本不起作用,除非您requireload文件或未编译的东西。但是如果他们被编译了,那么他们将无法使用它们吗?

如果我的一些猜测是正确的,那么这意味着“宏可以使用哪些数据”和“函数可以使用哪些宏”之间存在很大差异,这取决于您是编译整个文件以便稍后运行还是一次解释一行文件(即读取、编译和评估一个又一个表达式)。

简而言之,似乎要将一行编译为无需进一步宏处理或其他任何内容即可执行的形式,您必须阅读、编译和运行前面的行。

再次记住,这些问题适用于编译 lisp,而不是解释它,你可以在它进入时运行每一行。

很抱歉我的漫无边际,但我是 lisp 的新手,想了解更多它是如何工作的。

4

3 回答 3

5

这实际上是一个有趣的问题,也是很多 Lisp 初学者都在努力解决的问题。造成这种情况的主要原因之一是一切都“按预期”工作,当您开始使用 Lisp 的更高级功能时,您才真正开始考虑这些事情。

对您的问题的简短回答是,是的,为了正确编译代码,必须执行一些先前的代码。注意一些这个词,这是关键。让我们举一个小例子。考虑一个包含以下内容的文件:

(print 'a)

(defmacro bar (x) `(print ,x))

(bar 'b)

正如您已经知道的那样,如果您COMPILE-FILE在此文件上运行,生成的.fasl文件将仅包含以下代码的编译版本:

(print 'a)
(print 'b)

“但是”,您可能会问,“为什么DEFMACRO在编译期间执行了表单,但PRINT没有执行表单?”。答案在 Hyperspec 第3.2.3节中进行了解释。它包含以下句子:

通常,出现在使用 compile-file 编译的文件中的顶级表单仅在加载生成的编译文件时进行评估,而不是在编译文件时进行评估。但是,通常情况下,需要在编译时评估文件中的某些表单,以便可以正确读取和编译文件的其余部分。

有一种表单可用于精确控制何时评估表单。您EVAL-WHEN用于此目的。事实上,这正是 Lisp 编译器实现DEFMACRO自身的方式。您可以通过从 REPL 中键入以下内容来查看 Lisp 是如何实现它的:

(macroexpand '(defmacro bar (x) `(print ,x)))

显然不同的 Lisp 实现会以不同的方式实现这一点,但关键重要的是它将定义包装在一个表单中:(eval-when (:compile-toplevel :load-toplevel :execute) ...). 这告诉编译器应该在编译文件时和加载文件时评估表单。如果它不这样做,您将无法在定义的同一文件中使用该宏。如果仅在编译文件时评估表单,则加载后将无法在其他文件中使用宏。

于 2012-07-19T05:38:04.747 回答
4

文件的编译在 Common Lisp 中定义:CLHS Section 3.2.3 File Compilation

编译时:要使用使用读取宏的表单,您必须使该读取宏实现对编译器可用。

通常,此类依赖关系是使用defsystem工具处理的,其中描述了系统(类似于项目)的各种文件之间的依赖关系。为了编译某个文件,必须将另一个文件(最好是编译后的版本)加载到编译的 Lisp 中。

现在,如果你想定义 read 宏并在同一个文件中使用它的表示法,那么你再次需要确保编译器知道 read 宏及其实现。文件编译器有编译环境。默认情况下,它不会将同一文件的编译函数加载到此环境中。

为了让编译器知道它编译 Common Lisp 提供的文件中的某些代码EVAL-WHEN

让我们看一个读取宏示例:

(set-syntax-from-char #\] #\)) 

(defun reader-example (stream char)
  (declare (ignore char))
  (let ((class (read stream t nil t))
        (args (read-delimited-list #\] stream t)))
    (apply #'make-instance
           class
           args)))

(set-macro-character #\[ 'reader-example)

(defclass example ()
  ((name :initarg :name)))

(defvar *examples*
  (list [example :name e1]
        [example :name e2]
        [example :name e3]))

如果你加载上面的源代码,一切都很好。但是如果我们使用文件编译器,它不会在不先加载它的情况下进行编译。COMPILE-FILE例如,通过使用路径名调用函数来调用文件编译器。

现在编译文件:

(set-syntax-from-char #\] #\)) 

以上不会在编译时执行。新的语法更改在编译时将不可用。

(defun reader-example (stream char)
  (declare (ignore char))
  (let ((class (read stream t nil t))
        (args (read-delimited-list #\] stream t)))
    (apply #'make-instance
           class
           args)))

上面的函数被编译,但没有被加载。在后面的步骤中,编译器无法使用它的实现。

(set-macro-character #\[ 'reader-example)

同样,上面的表单没有被执行——只是生成了它的代码。

(defclass example ()
  ((name :initarg :name)))

编译器会记录该类,但以后不能创建它的实例。

(defvar *examples*
  (list [example :name e1]
        [example :name e2]
        [example :name e3]))

上面的代码会触发错误,因为 read 宏在编译时不可用 - 除非它之前已加载。

现在有两个简单的解决方案:

  • 将 read 宏的实现放在一个单独的文件中,并确保在任何使用 read 宏的文件之前编译和加载它。

  • EVAL-WHEN在需要在编译时生效的代码周围加上一个:

例子:

(EVAL-WHEN (:compile-toplevel :load-toplevel :execute)
  (do-something-also-at-compile-time))

上面将被编译器看到并执行。现在您必须确保代码在编译时具有它调用的所有内容(所有需要的定义)。

不用说:尽可能减少这种编译依赖是一种很好的风格。通常将所需的功能放在一个单独的文件中,并确保在编译使用它的文件之前将该文件编译并加载到编译 Lisp 中。

于 2012-07-19T05:48:28.257 回答
1

宏(包括读取宏)只不过是函数,它们的处理方式与所有其他函数相同。一旦编译了函数或宏,您就不需要保留源代码。

许多 Lisp 实现根本不会做任何解释。例如,默认情况下 SBCL 只会编译,即使eval. 一个重要的细微差别是 Common Lisp 编译是增量的(与单独编译相反,在许多 Scheme 实现和 C 和 Java 等语言中很常见),它允许您编译一个函数或宏并立即使用它,在同一个“编译单元”。

于 2012-07-19T05:31:57.643 回答