为什么在没有initvalue的情况下defvar作用域的工作方式有所不同?


10

假设我有一个名为elisp-defvar-test.el包含的文件:

;;; elisp-defvar-test.el ---  -*- lexical-binding: t -*- 

(defvar my-dynamic-var)

(defun f1 (x)
  "Should return X."
  (let ((my-dynamic-var x))
    (f2)))

(defun f2 ()
  "Returns the current value of `my-dynamic-var'."
  my-dynamic-var)

(provide 'elisp-dynamic-test)

;;; elisp-defvar-test.el ends here

我加载此文件,然后进入暂存缓冲区并运行:

(setq lexical-binding t)
(f1 5)
(let ((my-dynamic-var 5))
  (f2))

(f1 5)按预期返回5,表示的主体按预期f1my-dynamic-var视为动态范围变量。但是,最后一种形式给出的变量变量为void my-dynamic-var,表明该变量使用词法作用域。似乎与的文档不一致,该文档defvar说:

defvar表格还声明变量为“特殊”,所以它始终是动态即使势必lexical-binding为t。

如果我更改defvar测试文件中的表格以提供初始值,则该变量将始终被视为动态变量,如文档所述。谁能解释为什么变量的作用域由defvar声明变量时是否提供初始值来确定?

如果很重要,这是错误回溯:

Debugger entered--Lisp error: (void-variable my-dynamic-var)
  f2()
  (let ((my-dynamic-var 5)) (f2))
  (progn (let ((my-dynamic-var 5)) (f2)))
  eval((progn (let ((my-dynamic-var 5)) (f2))) t)
  elisp--eval-last-sexp(t)
  eval-last-sexp(t)
  eval-print-last-sexp(nil)
  funcall-interactively(eval-print-last-sexp nil)
  call-interactively(eval-print-last-sexp nil nil)
  command-execute(eval-print-last-sexp)

4
我认为Bug#18059中的讨论是相关的。
罗勒

很好的问题,是的,请参阅错误#18059的讨论。
提请

我看到的,所以它看起来像文件将被更新,以解决这一问题在Emacs 26
莱恩C.汤普森

Answers:


8

为什么将两者区别对待主要是“因为这就是我们所需要的”。更具体地说,单参数形式defvar很早以前就出现了,但是比其他形式晚了,基本上是使编译器警告静音的“ hack”:在执行时它根本没有任何作用,因此,它是“意外”的意思沉默行为(defvar FOO)仅应用于当前文件(因为编译器无法知道这样的defvar已经在其他文件中执行过)。

lexical-binding在Emacs-24中引入时,我们决定重新使用(defvar FOO)表单,但这意味着它现在确实起作用。

部分保留了以前的“仅影响当前文件”的行为,但更重要的是允许一个库toto用作动态作用域的var,而又不阻止其他库toto用作按词法界定的var(通常,包前缀命名约定避免了那些冲突,但并没有在所有地方都用到),的新行为(defvar FOO)被定义为仅适用于当前文件,甚至进行了优化,因此它仅适用于当前范围(例如,如果它出现在函数中,则只会影响对该函数中的那个变量)。

从根本上说,(defvar FOO VAL)(defvar FOO)只是两个“完全不同”的事情。由于历史原因,它们恰好使用相同的关键字。


1
+1作为答案。但是恕我直言,Common Lisp的方法更清晰,更好。
提请

@Drew:我大都同意,但是重用(defvar FOO)新模式会使新模式与旧代码更加兼容。另外,IIRC与CommonLisp解决方案有关的一个问题是,对于像Elisp这样的纯解释器来说,这是非常昂贵的(例如,每次评估a时,let您都必须查看其内部,以防万一declare有影响某些var的情况)。
Stefan

双方都同意。
提请

4

根据实验,我相信问题在于(defvar VAR)没有init值只会影响它所出现的库。

当我添加(defvar my-dynamic-var)*scratch*缓冲区时,该错误不再发生。

我最初认为这是因为要评估该表格,但是我首先注意到只需访问存在该表格的文件就足够了;而且,仅在缓冲区中添加(或删除)该形式而不进行评估就足以更改使用来(let ((my-dynamic-var 5)) (f2))在同一缓冲区内进行评估时发生的情况eval-last-sexp

(我对这里发生的事情没有真正的了解。我发现这种行为令人惊讶,但是我不了解如何实现此功能的详细信息。)

我要补充一点,这种形式defvar(没有init值)可以防止字节编译器抱怨正在编译的elisp文件中使用了外部定义的动态变量,但是就其本身而言,不会导致该变量成为boundp;因此它不是严格定义变量。(请注意,如果为变量 boundp则根本不会发生此问题。)

在实践中,我想这可以解决,只要您确实(defvar my-dynamic-var)任何包含使用my-dynamic-var变量的词法绑定库包含在内(可能会在其他地方定义一个真实的定义)。


编辑:

感谢@npostavs中的指针在注释中:

两者eval-last-sexpeval-defun用于eval-sexp-add-defvars以:

将EXP defvar之前的所有s放在缓冲区前面。

具体地说,它查找所有defvardefconstdefcustom实例。(即使被注释掉,我也注意到。)

由于这是在调用时搜索缓冲区,因此说明了即使不进行评估,这些形式也如何在缓冲区中起作用,并确认该形式必须出现在同一elisp文件中(并且早于要评估的代码) 。


2
IIUC,错误号18059确认您的实验。
罗勒

2
似乎eval-sexp-add-defvars在缓冲区文本中检查defvars。
npostavs

1
+1。显然,此功能不清楚,或者没有清楚地呈现给用户。错误#18059的文档修复有帮助,但是对于用户而言,即使不是脆弱的问题,这仍然是一个神秘的事物。
提请

0

我根本无法重现此错误,评估后面的代码段在这里工作正常,并按预期返回5。您确定您不会my-dynamic-var自己进行评估吗?这将引发错误,因为变量为void,尚未将其设置为值,并且如果将其动态绑定到一个值,则只会有一个值。


1
lexical-binding在评估表单之前,您是否设置了非零值?我得到您用lexical-bindingnil 描述的行为,但是当我将其设置为non-nil时,我得到了void变量错误。
瑞安·汤普森

是的,我将其保存到一个单独的文件中,还原,检查是否lexical-binding已设置并按顺序评估表单。
wasamasa

@wasamasa为我转载,也许您不小心my-dynamic-var在当前会话中提供了顶级动态值?我认为这可能将其永久地标记为特殊。
npostavs
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.