我大概知道为啥了…
上面,我们使用的都是 Emacs 29.1,在 Emacs 28.2 上,你给出的两段代码都是可以正常运行的。想来想去只能是 Emacs 29.1 引入的一个东西导致的: gnu.org/software/emacs/news/NEWS.29.1
* Lisp Changes in Emacs 29.1
** Interpreted closures are “safe for space”.
As was already the case for byte-compiled closures, instead of capturing
the whole current lexical environment, interpreted closures now only
capture the part of the environment that they need.
The previous behavior could occasionally lead to memory leaks or
to problems where a printed closure would not be 'read’able because
of an un’read’able value in an unrelated lexical variable.
在 Emacs 28.2 中,位于 (let (_)
中的 (defvar sym)
的 sym
会和 _
一起添加到词法环境中,下面的例子可以说明这一点:
(let (_)
(defvar yyy)
(lambda ()
(+ yyy 1)))
=> (closure (yyy (_) t) nil (+ yyy 1))
而在 Emacs 29 中,我们得到的是如下结果:
(let (_)
(defvar yyy)
(lambda () (+ yyy 1)))
=> (closure (t) nil (+ yyy 1))
我简单对比了一下 28 和 29 的 function
实现,发现 29 多了以下内容:
if (NILP (Vinternal_make_interpreted_closure_function))
return Fcons (Qclosure, Fcons (Vinternal_interpreter_environment, cdr));
else
return call2 (Vinternal_make_interpreted_closure_function,
Fcons (Qlambda, cdr),
Vinternal_interpreter_environment);
在 28 中没有 if,只有 Fcons 分支部分的代码。这个多出来的 Vinternal_make_interpreted_closure_function
应该做一些清理工作,去掉无用的词法环境变量。它绑定的函数位于 cconv.el
中,为 cconv-make-interpreted-closure
。它主要有这几个特点:
(下面使用 trace-function 'cconv-make-interpreted-closure)
观察其行为:
(let ((lexvars (delq nil (mapcar #'car-safe env))))
(if (null lexvars)
;; The lexical environment is empty, so there's no need to
;; look for free variables.
`(closure ,env . ,(cdr fun))
这也对应 (let ()...
的情况:
(let ()
(defvar yyyy)
(lambda ()
(+ 1 yyyy)))
======================================================================
1 -> (cconv-make-interpreted-closure (lambda nil (+ 1 yyyy)) (yyyy t))
1 <- cconv-make-interpreted-closure: (closure (yyyy t) nil (+ 1 yyyy))
- 如果环境中存在词法绑定则继续,具体过程大概是寻找函数中 所需 的词法变量和动态变量,然后重新组合得到环境,寻找所需词法和动态变量的过程是通过调用
cconv-fv
完成的:
(fvs (cconv-fv expanded-form lexvars dynvars))
比较有意思的是它返回的动态变量列表似乎 总为空 ,它的返回值结构为 (LEX_LIST . DYN_LIST)
,例如如下调用:
(cconv-fv '((x) (+ x y a b)) '(y) '(a b))
=> ((y))
你可以在 Emacs 29 中多试几个例子。
动态变量列表总为空就导致生成闭包的 env 中没有动态变量,从而导致 byte-compile
时出现 warning
看上去很吓人是吧,不过整个 Emacs 29 既然都能正常使用那就说明这是没问题的。维护者能知道能这样做就行。老实说 LZ 的样例代码不是什么常规用法,还是像 LdBeth 所说遵守约定正常使用就行,不用在边缘情况下耍。虽然会报 Warning 但是生成的字节码是没有问题的,可以 disassemble
对比一下。
不过也很好玩就是了。