在Common Lisp中,什么时候需要使用eval-when,你怎么知道?

时间:2017-08-14 00:08:10

标签: common-lisp compile-time eval-when

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

编辑: 正如您从答案中看到的那样,这个例子存在一个问题:它在编译时实际上没有调用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 C-k来评估和加载文件到repl,但是在这里,我使用compile-fileload命令来证明没有错误发生。 (当我尝试在编译之后但在加载之前使用这些函数时,我确实收到错误,但是任何卸载的代码都会出现错误。)

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

  • 这个previous StackOverflow answer似乎非常清楚地表明宏所使用的任何函数必须由eval-when表单括起来,或者加载到单独的文件中。

    < / LI>
  • coredump的评论也很清楚:

    扩展宏时,宏调用的任何函数都必须是 定义。如果你有一个定义宏的编译单元,那么 调用函数,但你实际上并没有使用宏 编译单元,你不需要eval-when。但是,如果您定义了一个 辅助。功能,一个宏,并想在你之后立即使用你的宏 定义它,然后实现可能会抱怨辅助。 功能未知 - coredump

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

感谢您的耐心等待!

1 个答案:

答案 0 :(得分:5)

<强>记住

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

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

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

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

还要记住,在编译文件时,如果编译器需要稍后调用其中的部分文件,则需要加载该文件。如果刚刚编译了一个函数,文件编译器将在编译时环境中存储代码,并且在完成文件编译后也不会。如果您需要执行代码,则需要加载已编译的代码 - &gt;或使用EVAL-WHEN - &gt;见下文。

您的代码

您的代码在编译时 调用函数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.

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

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

 )

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

相关问题