在Common Lisp中,何时需要使用eval,以及如何知道



eval-when的必要用途是确保宏所依赖的函数在编译和使用宏时可用。然而,我想不出一个例子可以证明不使用eval-when的后果。

(defpackage :eval-when
(:use :cl))
(in-package :eval-when)
(defun util-fun (x) (* x x))
(defmacro needs-help (x) `(let ((a (util-fun ,x))) a))
;; use it in the same file
(defun use-the-macro (x) (needs-help x))
(use-the-macro 5)

如果我理解正确的话,(defun util-fun ...)应该用eval-when包装。

EDIT:正如您将从Answer中看到的,这个例子有一个问题:它在编译时实际上并没有调用UTIL-FUN。这就解释了为什么没有给出错误,因为它不是错误。但这个问题仍然有效,因为它突出了新用户的困惑

但是,根据REPL,在编译、加载或使用过程中不会发出错误或警告(SBCL 1.3.20):

; SLIME 2.19
CL-USER> (uiop:getcwd)
#P"/home/anticrisis/dev/common-lisp/eval-when/"
CL-USER> (compile-file "eval-when.lisp")
; compiling file "/home/anticrisis/dev/common-lisp/eval-when/eval-when.lisp" (written 14 AUG 2017 11:30:49 AM):
; compiling (DEFPACKAGE :EVAL-WHEN ...)
; compiling (IN-PACKAGE :EVAL-WHEN)
; compiling (DEFUN UTIL-FUN ...)
; compiling (DEFMACRO NEEDS-HELP ...)
; compiling (DEFUN USE-THE-MACRO ...)
; compiling (USE-THE-MACRO 5)
; /home/anticrisis/dev/common-lisp/eval-when/eval-when.fasl written
; compilation finished in 0:00:00.009
#P"/home/anticrisis/dev/common-lisp/eval-when/eval-when.fasl"
NIL
NIL
CL-USER> (in-package :eval-when)
#<PACKAGE "EVAL-WHEN">
EVAL-WHEN> (use-the-macro 3)
; Evaluation aborted on #<UNDEFINED-FUNCTION USE-THE-MACRO {10035E1103}>.
EVAL-WHEN> (needs-help 4)
; Evaluation aborted on #<UNDEFINED-FUNCTION UTIL-FUN {100387FE33}>.
EVAL-WHEN> (load "eval-when.lisp")
T
EVAL-WHEN> (use-the-macro 3)
9
EVAL-WHEN> (needs-help 4)
16
EVAL-WHEN> 

注意,通常我使用C-C-k来评估并将文件加载到repl,但在这里,我使用compile-fileload命令来证明不会发生错误。(当我试图在编译后但在加载之前使用这些函数时,我确实收到了一个错误,但任何卸载的代码都会出现这种情况。)

之前有与此相关的问题和评论:

  • 前面的StackOverflow回答似乎非常清楚地表明,宏使用的任何函数都必须包含在eval-when表单中,或者加载在单独的文件中。

  • 这个来自coredump的评论也非常清楚:

    展开宏时,宏调用的任何函数都必须是定义如果您有一个定义宏的编译单元调用函数,但在编译单元,当。但是,如果您定义aux。函数,一个宏,并且希望在您之后立即使用您的宏定义它,那么实现可能会抱怨aux。功能未知–堆芯转储

既然如此,为什么我的示例没有生成错误?我的例子在其他情况下会失败吗?举一个编译时、加载时或未正确使用eval-when时生成的运行时错误的例子,将有助于我理解。

谢谢你的耐心!

记住

EVAL-WHEN在那里告诉文件编译器它是否应该在编译时执行代码(例如,对于函数定义,它通常不执行),以及它是否应该将编译后的代码安排在编译后的文件中以在加载时执行。这只适用于顶级表单。

Common Lisp在完整的Lisp环境中运行文件编译器(请记住,我们谈论的是编译文件,而不是在REPL中执行),并且可以在编译时运行任意代码(例如,作为开发环境工具的一部分,生成代码、优化代码等)。如果文件编译器想要运行代码,则文件编译器需要知道这些定义。

还要记住,在宏扩展期间,执行宏的代码以生成扩展的代码。宏本身调用来计算代码的所有函数和宏都需要在编译时可用。编译时不需要提供的是宏形式扩展到的代码。

这有时是一个混乱的来源,但它可以学习,然后使用它并不太难。但令人困惑的是,文件编译器本身是可编程的,并且可以在编译时运行Lisp代码。因此,我们需要理解代码可能在不同情况下运行的概念:在REPL中,在加载时,在编译时,在宏扩展期间,在运行时,等等

还要记住,当编译文件时,如果编译器稍后需要调用部分文件,则需要加载该文件。如果刚编译了函数,文件编译器将在编译时环境中存储代码,也不会在完成文件编译后存储代码。如果需要执行代码,则需要加载编译后的代码->或者使用CCD_ 10->请参见下文。

您的代码

您的代码不会在编译时调用函数util-fun。因此,函数是否需要在编译时环境中可用。

示例

另一个示例是实际调用函数的地方,请参见下文。这是一个Lisp文件中的代码,将由compile-file编译。

(defun run-at-compile-time ()
(print 'I-am-called-at-compile-time))
(defmacro foo ()
(run-at-compile-time)             ; this function is called for its
;   side-effect: it prints something
'(print 'I-am-called-at-runtime)) ; this code is returned
(foo)       ; we use the macro in our code, the compiler needs to expand it.
; Thus during macro expansion the function
; RUN-AT-COMPILE-TIME will be called.

因此,在宏扩展期间,宏foo喜欢调用在同一文件中定义的函数run-at-compile-time。由于它在编译时环境中不可用,因此这是一个错误。文件编译器只为要存储在磁盘上的函数生成代码,这样,当加载编译后的文件时,就会定义函数。但是它没有定义运行编译器的Lisp内部的函数->文件编译器无法调用它。

介绍EVAL-WHEN

要告诉编译器也让编译时环境知道它,您需要将它封装在EVAL-WHEN中,并添加:compile-toplevel情况。然后,当文件编译器在顶层看到函数时,它会运行定义宏。

(eval-when 
(:compile-toplevel  ; this top-level form will be executed by the
;  file compiler
:load-toplevel     ; this top-level form will be executed at load-time
;  of the compiled file
:execute)          ; executed whenever else
(defun run-at-compile-time ()
(print 'I-am-called-at-compile-time))
)

你也可以只提到其中的一两种情况。例如,当文件编译器在顶层看到该表单时,就可以执行该表单。它不会在加载时或其他情况下执行。

最新更新