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

ant*_*sis 4 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)
Run Code Online (Sandbox Code Playgroud)

如果我理解正确,(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> 
Run Code Online (Sandbox Code Playgroud)

请注意,通常我使用Cc Ck来评估和加载文件到repl,但在这里,我使用compile-fileload命令来证明没有错误发生.(当我尝试在编译后但在加载之前使用这些函数时,我确实收到错误,但任何卸载的代码都会出现这种情况.)

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

  • 以前StackOverflow的答案似乎很清楚地说,这是使用宏任何功能必须由封闭eval-when的形式,或在一个单独的文件中加载.

  • 来自coredump的评论也很清楚:

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

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

感谢您的耐心等待!

Rai*_*wig 6

记得

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

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

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

这有时是混乱的根源,但它可以学习,然后使用它并不太难.但令人困惑的部分是文件编译器本身是可编程的,可以在编译时运行Lisp代码.因此,我们需要理解代码可能在不同情况下运行的概念:在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.
Run Code Online (Sandbox Code Playgroud)

因此在宏扩展期间,宏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))

 )
Run Code Online (Sandbox Code Playgroud)

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

  • “加载时间”(即`:load-toplevel`)和“其他时候”(`:execute`)有什么区别?从 REPL 来看,它们是同一件事。 (3认同)
  • 非常棒的解释,谢谢,但是我想知道您是否可以澄清另一点:事实上,在实践中,如果您的宏定义位于与实际使用它们的文件分开的文件中,那么这个问题永远不会出现,并且 eval - 当没有必要时。我相信,这导致了我自己的困惑。 (2认同)