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

In Common Lisp, when do you need to use eval-when, and how do you know?

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 命令来证明没有错误发生。 (当我在函数编译之后但在加载之前尝试使用这些函数时,我确实收到了一个错误,但是任何卸载的代码都会发生这种情况。)

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

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

感谢您的耐心等待!

记得

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

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

另外请记住,在宏扩展期间,宏的代码会被执行以生成扩展代码。宏 自身 调用以计算代码的所有函数和宏都需要在编译时可用。不需要在编译时可用的是宏形式扩展到的代码。

这有时会造成混淆,但可以学习并使用起来并不难。但这里令人困惑的部分是文件编译器本身是可编程的,并且可以在编译时 运行 Lisp 代码。因此,我们需要理解代码可能在不同情况下 运行ning 的概念:在 REPL 中、加载时、编译时、宏扩展期间、运行 时等。

还要记住,当你编译一个文件时,你需要加载这个文件,如果编译器需要稍后调用它的一部分。如果函数刚刚编译,文件编译器将不会将代码存储在编译时环境中,也不会在文件编译完成后存储。如果需要执行代码,则需要加载编译后的代码 -> 或使用 EVAL-WHEN -> 见下文。

您的代码

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

 )

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