Emacs Lisp中评估表单序列为何出现不同结果?
问题描述
将以下Elisp代码粘贴到Emacs scratch缓冲区:
; Source - https://stackoverflow.com/a/28991218 ; Posted by quicoju ; Retrieved 2026-02-24, License - CC BY-SA 3.0 (setq the-empty-stream nil) (defun stream-null? (s) (eq s the-empty-stream)) (defun my/sicp-delay (exp) `(lambda () ,exp)) (defun my/sicp-force (exp) (funcall exp)) (defmacro my/cons-stream (a b) (list 'cons a (my/sicp-delay b))) (defun my/stream-car (stream) (car stream)) (defun my/stream-cdr (stream) (my/sicp-force (cdr stream))) (defun my/stream-enumerate-interval (low high) (let ((l low) (h high)) (if (> l h) the-empty-stream (my/cons-stream low (my/stream-enumerate-interval (1+ l) h)))))
使用M-x eval-buffer评估整个缓冲区后,执行表达式:
(my/stream-car (my/stream-cdr (my/stream-cdr (my/stream-enumerate-interval 10 20))))
会报错:
Debugger entered--Lisp error: (void-variable l) (1+ l) (my/stream-enumerate-interval (1+ l) h) . . .
但如果手动逐个用C-M-x评估每个表单后执行该表达式,能得到正确结果12。这是什么原因?
原因分析
核心问题出在**my/sicp-delay的定义类型和Emacs Lisp编译/解释模式的作用域差异**:
SICP中实现流的
delay必须是宏,因为它需要捕获未求值的表达式,避免立即执行。但你的代码里my/sicp-delay被定义成了函数——函数的参数会被立即求值,这本身就不符合流的延迟语义,只是在解释模式下出现了特殊的可运行状态。当用
M-x eval-buffer时,整个缓冲区会被编译为字节码:- 宏
my/cons-stream会被提前展开,my/stream-enumerate-interval里的代码会变成(cons low (my/sicp-delay (my/stream-enumerate-interval (1+ l) h)))。 - 编译器处理这段代码时,会识别出
l和h是let绑定的词法局部变量,而my/sicp-delay返回的lambda是延迟执行的——当后续my/sicp-force调用这个lambda时,原let的作用域早已消失,编译后的字节码无法捕获这些词法变量,因此运行时会触发void-variable l的错误。
- 宏
当用
C-M-x逐个评估表单时,代码以解释模式运行:- Emacs Lisp解释模式默认使用动态作用域,lambda里的
l和h会在动态作用域链中查找。 - 加上解释模式对函数参数和反引号的处理更宽松,
(my/stream-enumerate-interval (1+ l) h)没有被立即求值,而是被保留为表达式插入到lambda中。当my/sicp-force调用lambda时,动态作用域中还能找到l和h的绑定,因此能正确执行并返回结果12。
- Emacs Lisp解释模式默认使用动态作用域,lambda里的
解决方法
把my/sicp-delay从函数改为宏,这样就能正确捕获未求值的表达式,无论编译还是解释模式都能正常工作:
(defmacro my/sicp-delay (exp) `(lambda () ,exp))
内容的提问来源于stack exchange,提问作者wejaeger
相关产品推荐
相关产品推荐

