如何理解 magit 源码中的 (cl-eval-when (load eval) ...) ?

请问magit源码中这里的require为什么要这样放置?

(cl-eval-when (load eval)
  (require 'magit-status)
;;; ...
  )

一种等价的方式是

(progn 
  (require 'magit-status)
;;; ...
  )

后面这种方式应该如何理解?

require自身已经被包裹在(cl-eval-when (compile load eval))三种状态中,这里为何要专门用(cl-eval-when (load eval))包裹?

下面是cl-eval-when的源码

;;;###autoload
(defmacro cl-eval-when (when &rest body)
  "Control when BODY is evaluated.
If `compile' is in WHEN, BODY is evaluated when compiled at top-level.
If `load' is in WHEN, BODY is evaluated when loaded after top-level compile.
If `eval' is in WHEN, BODY is evaluated when interpreted or at non-top-level.

\(fn (WHEN...) BODY...)"
  (declare (indent 1) (debug (sexp body)))
  (if (and (macroexp-compiling-p)
	   (not cl--not-toplevel) (not (boundp 'for-effect))) ;Horrible kludge.
      (let ((comp (or (memq 'compile when) (memq :compile-toplevel when)))
	    (cl--not-toplevel t))
	(if (or (memq 'load when) (memq :load-toplevel when))
	    (if comp (cons 'progn (mapcar #'cl--compile-time-too body))
	      `(if nil nil ,@body))
	  (progn (if comp (eval (cons 'progn body) lexical-binding)) nil)))
    (and (or (memq 'eval when) (memq :execute when))
	 (cons 'progn body))))

cl-eval-when的认识:

  1. 顶层形式
  2. (cl-eval-when (compile) body ...) 在编译阶段,body在顶层形式编译时求值;
  3. (cl-eval-when (load) body ...) 在加载阶段,body在顶层形式编译后求值;
  4. (cl-eval-when (eval) body ...) 等同于progn

修改标题:

- magit cl-eval-when
+ 如何理解 magit 源码中的 (cl-eval-when (load eval) ...) ?

因为这样在外边包一层可以防止 require 展开的 cl-eval-when 生效。后果是阻止里面的 require 在编译时运行。

cl-eval-when 不是 toplevel 时不会生效。这个在 emacs 的文档里虽然没写,但是根据 Common Lisp 来的。

eval-when normally appears as a top level form , but it is meaningful for it to appear as a non-top-level form . However, the compile-time side effects described in Section 3.2 (Compilation) only take place when eval-when appears as a top level form .

http://www.lispworks.com/documentation/HyperSpec/Body/s_eval_w.htm#eval-when

你这些认识只是在复读定义罢了,并不能帮助你了解它。

它的作用说实在点,就是控制表达式会不会出现在编译以后的文件里出现,和编译的时候会不会运行一遍(正常情况下编译是不会运行表达式的)。

1 个赞

感谢您的回复哈。

  1. 顶层形式
  2. (cl-eval-when (compile) body …) 在编译阶段,body在顶层形式编译时求值;
  3. (cl-eval-when (load) body …) 在加载阶段,body在顶层形式编译后求值;
  4. (cl-eval-when (eval) body …) 等同于progn

把4条分开看,这些是直观的。把4条合在一起看会出现不一致性。

  1. (cl-eval-when (eval) body ...)等价于progn

  2. 在Emacs文档关于cl-eval-when叙述中有“ Note that (cl-eval-when (load eval) …) is equivalent to (progn …) in all contexts.”

    依据1,2是否可以得出(cl-eval-when (load) body...)等价于progn呢?

    即是否可以得出(cl-eval-when (eval) body ...)(cl-eval-when (load) body...)是等价的?

  3. progn是顶层形式,progn包裹的内容在编译时会求值。

    依据1,3是否可以得出(cl-eval-when (eval) body ...)(cl-eval-when (compile) body...)是等价的?

不一致性的根源在于progn等价转换,请问这上面哪一条陈述是有错误的?

期待您的回复。

感谢您的编辑。

你真是逻辑鬼才😰

不能,从 propositional logic 的角度,(a /\ b) = c ==> a = c ==> b = c 不能证明,可以找到反例。

不会,没有任何文档提及 progn 有编译时求值。

这两条错了,得出的结论当然有问题。

实战感觉只要把那些 "为了用户方便而require子包"的require 语句放在 provide 后面就行, 并不需要 cl-eval-when

;; foo.el

(provide 'foo)

(require 'bar)
;; bar.el

(require 'foo)

(provide 'bar)

感谢您的耐心回复哈。

  1. (cl-eval-when (eval) body ...) 等价于 progn
  2. 在Emacs文档关于 cl-eval-when 叙述中有“ Note that (cl-eval-when (load eval) …) is equivalent to (progn …) in all contexts.”

依据1,2,magit源码中是否可以把

(cl-eval-when (load eval)
  (require 'magit-status)
  ;;; ...
  )

写为

(cl-eval-when (eval)
  (require 'magit-status)
  ;;; ...
  )

实验结果显示不可以,请问1, 2叙述是哪里有问题?

感谢小伙伴的回复哈。把cl-eval-when删除可正常工作。

只在 interpret 等于 progn,和 (load eval) 的区別是不会在编译后的文件里出现。

感谢您的回复哈,我再看看cl-eval-when的源码。

感谢您的回复哈,我看了一下cl-eval-when的源码。 源码中显示的是(cl-eval-when (eval) body ...) 始终等价于 progn,不仅是interpret。 请问应该如何理解?

你没看到 (if (and (macroexp-compiling-p … 么,意思就是在编译的时候,只会在 when 里面有 compileload 的时候做处理,只有 eval 的话只会给 nil,哪来的

最简单的证实方法,新开个 elisp 文件分別写上 evaleval load 看编译结果,只有第二个在 elc 里出现。

不要猜,多实验。学习最忌讳动口不动手。

;; bar.el

(require 'cl-lib)

(cl-eval-when (eval)
  (message "I'm running on eval!"))
(cl-eval-when (load)
  (message "I'm running on load!"))
(cl-eval-when (compile)
  (message "I'm running on compile!"))
;; Run `(byte-compile-file "bar.el")`
;;  => I'm running on compile!
;; Run (load-file "bar.el")
;; => I'm running on eval!

;; Open the bar.el file and run `M-x eval-buffer`
;; => I'm running on eval!

;; Run byte compile and `(load-file "bar.elc")`
;; => I'm running on load!

;; Open the byte-compiled file (bar.elc)
;; => (byte-code "\300\301!\210\302\303!\207" [require cl-lib message "I'm running on load!"] 2)

;; Eval the byte-code above
;; => I'm running on load!

Emacs 的 cl-eval-when 和 CL 的 eval-when 大相径庭。除了 compile 是指“编译 Elisp 代码”这个状态,loadeval 指的是运行的代码的一种状态,简单解释下三个 phase 各自代表的含义:

  • eval 代表代码在 未编译 时(即 bar.el 文件)时运行,至于怎么运行,你可以 eval-last-sexp 可以 eval-buffer,也可以 load-file,无论怎么运行,只要代码还是未编译的 S-exp,那么这片代码就会被运行。编译后的代码,这个 block 包裹的代码 不会 运行

  • load 代表所包裹的代码,会被留到 编译后的 .elc 文件 里面,但是直接 eval 的话也不会运行。当你编译完 bar.el 后,通过 load-file 运行 bar.elc,就会运行 load 块包裹的代码。当然了,因为编译之后的代码已经移除了 cl-eval-when,那么直接 eval 编译生成的 byte code 也会打印 I'm running on load!

  • compile 就是编译时会被运行的代码

3 个赞

你看 cl-eval-when 的源码没用,这个源码里面有部分功能需要 bytecomp.el 里面的代码配合,好比你看

(defmacro eval-when-compile (&rest body)
  "Like `progn', but evaluates the body at compile time if you're compiling.
Thus, the result of the body appears to the compiler as a quoted
constant.  In interpreted code, this is entirely equivalent to
`progn', except that the value of the expression may be (but is
not necessarily) computed at load time if eager macro expansion
is enabled."
  (declare (debug (&rest def-form)) (indent 0))
  (list 'quote (eval (cons 'progn body) lexical-binding)))

(defmacro eval-and-compile (&rest body)
  "Like `progn', but evaluates the body at compile time and at
load time.  In interpreted code, this is entirely equivalent to
`progn', except that the value of the expression may be (but is
not necessarily) computed at load time if eager macro expansion
is enabled."
  (declare (debug (&rest def-form)) (indent 0))
  ;; When the byte-compiler expands code, this macro is not used, so we're
  ;; either about to run `body' (plain interpretation) or we're doing eager
  ;; macroexpansion.
  (list 'quote (eval (cons 'progn body) lexical-binding)))

这俩玩意从源码上看一模一样,实际效果就完全不同,那不是刻舟求剑?

(cl-eval-when (eval) body ...)progn 不等价,一方面,progn 在 byte compiler 里面有特殊处理,如

(progn
  (require 'foo)
  (require 'bar)
  (require 'baz))

编译器可以解开 progn 查看里面的 body,然后按照 body 里面的 require 的规则去进一步编译。而编译器不能解开 cl-eval-when

另一方面,progn 代表的就是直接 eval 会运行,编译成 bytecode 的时候也会留到里面的代码。(cl-eval-when (load eval) ...) 可以理解为一种受限制的 progn


顺便说一下 eval-when-compileeval-and-compile

eval-and-compile 在任意时期,无论是编译期还是直接运行未编译的代码,或者运行编译后的 bytecode. 这个 form 都必须要 eval,这个等效于 (cl-eval-when (load eval compile) ...)

eval-when-compile 表示在编译时会运行其中结果,并把运算好的结果写入 byte code 里面。

;;; test.el 

(message "%s"
         (eval-when-compile
           (buffer-name (current-buffer))))

;;; test.elc
;;; buffer name is hard-coded into the byte code.

(message "%s" " *Compiler Input*")

如当在非编译期执行该 form,那么正常 eval,不加额外处理。这东西无直接的 cl-eval-when 对应


我个人完全不理解,magit 里面使用 cl-eval-when 为什么可以做到避免 recurisve require,事实上这也不完全和 cl-eval-when 有关,和 provide 的先后顺序也有关系。我个人也从不建议别人在代码里面使用 cl-eval-when。避免 recursive require 有更好,更简单的做法(如更细粒度的拆文件,或者直接用 declare-function 把函数未定义的警告压住)

Emacs 目前的执行机制,解释执行和编译后执行还是分开的(不像很多 Lisp 解释和编译已经融合在一起了)。cl-eval-when 的组合里,除了 (compile eval load)(eval load) 两个组合以外,其他组合都会导致你的代码在不同的执行方式下出现不同的 eval 结果,这玩意除了给你自己和别人造成麻烦以外毫无意义。

3 个赞

错了,cl-eval-when 不用 byte compiler 特殊处理就能实现。把它的定义复制一遍換个名字一样能用,而 eval-and-compile 換个名字就不行了。不是存在隐藏的 hack,就是单纯的把自已看不懂的代码忽略掉只看到看得懂的就下结论罢了。

在 Common Lisp 里对应 #. reader macro。

If progn appears as a top level form , then all forms within that progn are considered by the compiler to be top level forms .

假的,或者说取決于展开后是否是 progn

#. 可以

(case 20
  #.(list 20 ''foo)
  (30 'bar))
;; => foo

eval-when-compile 只能当值用

直接设计成了不能解开,展开结果为 (if nil nil ,@body),除了设计如此,想不到任何不直接写 progn 的理由 只在只有 load 的情况下会有这种展开。那更无法理解为什么要用 cl-eval-when 去避免 recursive require 了

https://lists.gnu.org/archive/html/emacs-devel/2022-04/msg01259.html

对此有兴趣的也可以来这里讨论

这个理解是错的,哪怕 eval 用的是 compiler,一样会保留区分 macroexpand time 和 execution time 的区别。连 Scheme 都保留了 macroexpand 产生 side effect 的能力。