Emacs24中的词法评估

4

有人能给我解释一下在Emacs24中如何使用eval吗?根据eval的描述:

eval is a built-in function in `C source code'.

(eval FORM &optional LEXICAL)

Evaluate FORM and return its value.
If LEXICAL is t, evaluate using lexical scoping.

这是否意味着这样的东西应该能够正常工作?
(setq lexical-binding t)
(let ((myvarr 42)) (eval 'myvarr t)) ; (void-variable myvarr)

更新:

(setq lexical-binding nil)
;; => nil
(let ((myvarr 42)) (eval 'myvarr))
;; => 42 (#o52, #x2a, ?*)
(setq lexical-binding t)
;; => t
(let ((myvarr 42)) (eval 'myvarr))
;; Debugger entered--Lisp error: (void-variable myvarr)
;;   eval(myvarr)
;;   (let ((myvarr 42)) (eval (quote myvarr)))
;;   (progn (let ((myvarr 42)) (eval (quote myvarr))))
;;   eval((progn (let ((myvarr 42)) (eval (quote myvarr)))) t)
;;   eval-last-sexp-1((4))
;;   eval-last-sexp((4))
;;   call-interactively(eval-last-sexp nil nil)
;;   call-last-kbd-macro(nil kmacro-loop-setup-function)
;;   kmacro-call-macro(nil nil)
;;   kmacro-end-or-call-macro(nil)
;;   call-interactively(kmacro-end-or-call-macro nil nil)
(ignore-errors (let ((myvarr 42)) (eval 'myvarr)))
;; => nil
(setq lexical-binding nil)
;; => nil
(eval (let ((myvarr 42)) (eval 'myvarr)) t)
;; => 42
(eval '(let ((myvarr 42)) (eval 'myvarr)) t)
;; Debugger entered--Lisp error: (void-variable myvarr)
;;   eval(myvarr)
;;   (let ((myvarr 42)) (eval (quote myvarr)))
;;   eval((let ((myvarr 42)) (eval (quote myvarr))) t)
;;   eval((eval (quote (let ((myvarr 42)) (eval (quote myvarr)))) t) nil)
;;   eval-last-sexp-1((4))
;;   eval-last-sexp((4))
;;   call-interactively(eval-last-sexp nil nil)

Emacs版本:GNU Emacs 24.1.1(i386-mingw-nt6.1.7600),发布于2012年6月10日,运行在MARVIN平台上。


你想要做什么或者理解什么?你的代码预期的行为是什么? - Nicolas Dudebout
3个回答

7

由于词法绑定会破坏很多现有的elisp代码,因此它是一项选择性功能。

词法作用域可以通过一个简单的例子来更好地理解:

(defun some-func (callback)
 (let ((a 5))
  (funcall callback)))

(let ((a 3))
 (some-func (lambda () a)))

在大多数语言中,这将返回 3,因为在底部表单中看起来似乎some-func中的 a不可见。然而,在 emacs 24 之前或没有词法作用域的情况下,此程序返回 5。
这导致许多意外惊喜和微妙而常常隐藏的错误在交互函数之间产生;为了解决这个问题,emacs 24 引入了词法作用域,但正如先前提到的,它是向后兼容的。
选择使用动态作用域的机制可以是文件变量 lexical-binding(它可以按源文件打开)或者像你所看到的 eval选项一样。
因此,如果我们重写示例以使用 eval
(eval '(let ((a 3))
        (some-func (lambda () a))) nil) ; => 5

(eval '(let ((a 3))
        (some-func (lambda () a))) t) ; => 3

在你的示例中,决定差异的不是eval内部代码是否动态作用域,而是其周围的代码是否如此。变量绑定受词法作用域影响,而不是变量查找。我并不完全确定eval在这里的语义,但似乎正在发生的事情(也是最有意义的)是eval在一个全新的词法上下文中评估表达式。因此,外部词法范围对eval内部是隐藏的,但动态作用域仍然可见(因此,当文件具有动态作用域时,查找成功,否则不成功)。

我更新了原始帖子。那种行为可以解释吗?更新:是的,我的错。这是完全清楚的,它不会返回42。 - desudesudesu
发现使用这个座右铭非常有用:“在创建闭包时使用 (eval '(let ... ) t)”。这样,您可以同时使用动态和词法作用域的功能。 - desudesudesu
1
@desudesudesu:一般来说,如果你需要使用eval函数,那么你可能正在做一些错误的事情。 - Stefan
你不需要使用 eval 来结合动态作用域和词法作用域:只需在文件局部变量中将 lexical-binding 设置为 t 即可。然后使用 defvar 声明需要使用动态作用域的变量。问题并不是 eval 可以以有害方式使用,而是 eval 本身具有许多弊端,因此只有在真正需要时才应使用它。 - Stefan
@Stefan 这样所有函数的参数都将是词法作用域,而且并不是你总是想要的。我真的不喜欢它成为全局变量,我喜欢它在elisp中的方式,而且我讨厌CL的变体。eval有哪些缺点?它可以像这里一样提供更多信息吗? - desudesudesu
显示剩余4条评论

6
我在Emacs 24中找不到eval函数的正式语义,但是假设它与Common Lisp中的语义相同(由cobbal指出),你提供的所有示例都是有意义的。根据Common Lisp HyperSpec的描述:

语法:

eval form

描述:

在当前动态环境和null词法环境中评估form

让我们逐个查看您的示例并考虑以上描述。

(setq lexical-binding t)
(let ((myvarr 42)) (eval 'myvarr t)) ; Lisp error: (void-variable myvarr)

启用词法绑定需要将t设置为lexical-binding,因此myvarr变成了一个词法绑定变量,无法在上面提到的eval函数中使用。 eval函数的选项t在这里是无关紧要的。

(setq lexical-binding nil)
(let ((myvarr 42)) (eval 'myvarr)) ; 42

lexical-binding设置为nil将禁用词法绑定,因此myvarr变成了一个动态绑定的变量,在eval函数内可用。 eval函数的选项,隐含地为nil,在这里是不相关的。

(setq lexical-binding t)
(let ((myvarr 42)) (eval 'myvarr)) ; Lisp error: (void-variable myvarr)

启用词法绑定(Lexical binding)需要将t设置为lexical-binding,因此myvarr成为一个词法绑定变量,在eval函数内部不可用。 eval函数的选项默认为nil,在这里无关紧要。

(ignore-errors (let ((myvarr 42)) (eval 'myvarr))) ; nil

同上。
(setq lexical-binding nil)
(eval (let ((myvarr 42)) (eval 'myvarr)) t) ; 42

通过将 lexical-binding 设置为 nil,禁用了词法绑定,因此 myvar 变成了一个动态绑定的变量,在内部的 eval 函数中可用。请注意,包括内部的 eval 函数在内的 let 表单会在调用外部的 eval 之前作为参数准备进行评估。这里既不涉及外部也不涉及内部的 eval 函数选项的相关性。
(eval '(let ((myvarr 42)) (eval 'myvarr)) t) ; Lisp error: (void-variable myvarr)

myvarr变成了一个词法绑定变量,在内部的eval中不可用。需要注意的是,由于'let形式通过启用词法绑定的外部eval函数进行评估。这里相关的是外部eval函数的选项,而内部eval函数的选项则无关紧要。


为什么要使用空的词法环境?

我认为,这是因为如果使用当前的词法环境,则α-等价将不再保持。

α-等价是一种正式的说法,意思是函数参数的名称并不重要。例如,(lambda (x) x)(lambda (y) y)是α-等价的,我们认为它们是相同的。α-等价允许我们随时更改函数参数的名称。我们视其为理所当然的事情,如果不满足这点,我们会感到非常惊讶。有关更多正式解释,请参见Lambda演算α-等价性

但是,当涉及到包含自由变量(称为开放代码)的代码值可以在Lisp中传递时,α-等价性就会出现一些问题。让我们看下面的例子:

;;; -*- lexical-binding: t -*-
(lambda (x) (lambda (y) (eval x))) '(1+ y)

如果eval是在当前词法环境下进行求值,则该形式等同于(lambda (y) (1+ y))。现在看看下面这个程序:

(lambda (x) (lambda (z) (eval x))) '(1+ y)

这个程序与之前的程序唯一不同的是参数名称,y被替换为z,因此我们自然希望它们的行为方式相同。但是后者的程序计算结果为(lambda (z) (1+ y)),这与(lambda (y) (1+ y)明显不同。想想当应用相同的参数值时会发生什么。关键在于当允许包含自由变量的代码值时,函数参数的名称确实很重要。
为了保持α等价性,我们有两个选择:我们不能放弃α等价性,因为它感觉非常自然,而且我们已经习惯了它。第一个选择是像(Common)Lisp那样在空词法环境下评估eval。使用此选项,(1+y)中的自由变量y不会绑定到(lambda(y)...)中的形式参数y。所以这两个程序的行为是一致的,α等价性得到了很好的保持。另一种选择是从一开始就防止问题,不允许开放代码值。我听说这是MetaOCaml选择的方法。
有人可能会问:“那么,为什么当前有动态环境呢?”对于动态绑定(也称为特殊)变量,我们已经充分认识到它们的名称非常重要,如果您不小心更改它们,程序将会出错。

是的,我已经完全理解了。在第一篇帖子中有一个“更新”,我意识到我忘记引用eval,接下来的回答中有人向我解释了(eval arg t)的特性。但我仍然不明白为什么他们让“eval”以那种方式起作用(请参见我对Jisang Yoo的评论)。 - desudesudesu
你认为 eval 应该如何运作? - dkim
为什么不在词法环境中查找变量?有任何可选标志来控制这种行为吗?它并非在编译时构建,实际上我可以通过宏获取这些变量。那么,为什么呢? - desudesudesu
@desudesudesu,我已经添加了关于“为什么使用null词法环境”的解释。希望它能回答你的问题。 - dkim
嗯,是的。但我真的不明白为什么这么重要,居然没有可选项或至少创建另一个函数。 - desudesudesu
1
我担心我可能没有很好地理解你的疑问。如果我们想要一个变量动态地行为,我们可以使用动态变量吗? - dkim

2
你内部的eval表现出的奇怪行为看起来类似于词法绑定下的symbol-value或者add-to-list
参考链接:Emacs 词法作用域和带引号的变量名 在词法绑定下,一个符号的值仅保留全局值(可能与变量的词法绑定值不同)。使用带引号的变量(例如,(eval 'var)(eval '(print var))(add-to-list 'var 1))将只在符号的值单元上进行获取或设置操作。避免遇到此类问题的最佳实践是:
  • 尽可能使用宏而非 eval 进行定义和使用。

  • 如果要创建/声明全局变量,请避免使用setq,而是使用 defvar, defcustomdefconst 以特殊变量的形式创建。特殊变量始终是动态作用域的,即使在词法绑定模式下也是如此。


我就是不明白:为什么要这样做。我可以编写宏来完成所有正确的事情。为什么要试图如此接近CL? (defmacro symbol-value-in-lexical-closure(arg) `(cdr (assoc ,arg (cadr (lambda()))))) (defmacro super-eval(arg) `(or (symbol-value-in-lexical-closure ,arg) (eval ,arg))) - desudesudesu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接