Emacs Lisp中lexical-binding和defvar之间的奇怪交互作用

7
以下的Emacs Lisp文件是关于查看当Alice在她的init文件中使用一个词法绑定局部变量foo时会发生什么,而Bob在他的init文件中使用defvar定义了全局特殊变量foo,而Alice在不知道foo将变成特殊变量的情况下,借用了Bob的init文件代码的一部分到她自己的init文件中。
;; -*- lexical-binding: t; -*-
;; Alice init file

;; Alice defining alice-multiplier
(defun alice-multiplier-1 (foo)
  (lambda (n) (* n foo)))
(defun alice-multiplier-2 (num)
  (let ((foo num))
    (lambda (n) (* n foo))))

;; Alice using alice-multiplier
(print
 (list
  :R1 (mapcar (alice-multiplier-1 10) (list 1 2 3))
  :R2 (mapcar (alice-multiplier-2 10) (list 1 2 3))))

;; from Bob's code
;; ...    
(defvar foo 1000)
;; ...

;; Alice using alice-multiplier
(print
 (list
  :R3 (mapcar (alice-multiplier-1 10) (list 1 2 3))
  :R4 (mapcar (alice-multiplier-2 10) (list 1 2 3))))

输出:

(:R1 (10 20 30) :R2 (10 20 30))

(:R3 (10 20 30) :R4 (1000 2000 3000))

结果 R1 和 R2 正如我的预期。结果 R4 与 defvar 文档一致,尽管它可能会让 Alice 感到惊讶,除非她阅读了 Bob 的代码。

  1. 我觉得 R3 很令人惊讶。为什么 R3 是这样的?

  2. 说到 R4,Alice 能做些什么来保护她的 foo 不被他人变成特殊变量呢?例如,foo 可能是她在初始化文件或其中一个 Emacs 包中使用的词法局部变量,而 (defvar foo "something") 可能在她使用的某些包中出现,或者 foo 可能是未来版本的 Emacs 引入的新的特殊变量名称之一。Alice 能在她的文件中加入什么东西告诉 Emacs,“在这个文件中,即使外部代码使用了同名的特殊变量,foo 应该始终是词法变量”吗?


那不是来自外部的“代码”,对吧? - phils
@phils 我认为“外部代码”是指我可能会将从网上复制粘贴的代码放入我的.emacs文件中,而不是来自文件外部。 - Joshua Taylor
实际上,这可能会变得混乱,不是吗?我知道如果动态变量名称不包含某种前缀进行命名空间,字节编译器会抱怨,并且命名约定建议以这种方式为所有变量进行命名空间。我猜那最终就是答案,但这确实让人希望某种适当的命名空间已被引入以补充词法绑定更改... - phils
@phils 的“外部代码”可以是由 Bob 编写的 Emacs 包 superdired,其中 defvars foo,而 Mallory 可能会安装两个包 superdiredsupercalc,后者是由 Alice 编写的,提供类似于 alice-multiplier 的函数。当 Mallory 在一个 superdired 缓冲区中进行某些操作,然后在另一个不相关的任务中使用 alice-multiplier 时,如果 Alice 将 alice-multiplier 编写为 alice-multiplier-2,则 Mallory 将看到 R4。 - Jisang Yoo
2个回答

4

发生了什么

从“理论”(Scheme/Common Lisp)的角度来看,一旦启用词法绑定,实际上alice-multiplier-1alice-multiplier-2相同的。它们行为上的任何差异都是Emacs Lisp中的错误,应该报告给相关人员。

编译

如果你把你的代码(即两个defun; -*- lexical-binding: t; -*-行)放到一个文件中,使用emacs-list-byte-compile-and-load进行编译和加载,那么你可以通过执行以下4个表达式来测试我的说法:

(disassemble 'alice-multiplier-1)
(disassemble 'alice-multiplier-2)
(disassemble (alice-multiplier-1 10))
(disassemble (alice-multiplier-2 10))

您将看到数字3和4是相同的,而数字1和2则由于一条指令的差异而不同(这应该被报告给Emacs维护人员作为一个错误,但并不影响行为)。
请注意,所有的反汇编都没有提到foo,这意味着defvar不会影响它们的行为。
一切都很好!
解释:
实际上,您看到的行为是不正确的;在defvar之后,正确的结果是:
(:R1 (10000 20000 30000) :R2 (10000 20000 30000))

向Emacs维护者报告此问题

不同???

是的,defvar确实(并且应该!)影响解释代码的行为,但不应该影响编译代码的行为。

你应该怎么做

没有办法“保护”你的foo不被他人宣布为special,除非在“你的”符号前加上alice-前缀。

但是,如果使用alice-multiplier-1定义对文件进行字节编译,则编译后的文件甚至不包含foo,因此将来对foo的声明也不会影响你。


1
关于第二个问题,据我所知,没有。但仍然可以解决。这就是采用两种命名约定:我将称之为黄色和绿色。
黄色命名约定
所有特殊变量必须具有黄色名称。黄色名称是至少包含一个连字符的名称。例如,hello-world和ga-na-da是黄色名称。官方的Emacs Lisp手册和字节编译器鼓励使用此约定。
绿色命名约定
绿色名称是不是黄色的名称。例如,helloworld和ganada是绿色名称。
所有词法非本地/自由变量必须具有绿色名称。什么是非本地变量?alice-multiplier-2中的匿名函数体提到了三个名称,n、foo和num。其中,只有n在函数体(匿名函数)内部有声明。其他两个从匿名函数的角度来看都是非本地的。它们是非本地变量。
只要Alice和Bob遵循这两种命名约定,一切都好。即使他们现在不遵循,最终这两个人也很可能会自行收敛到这些约定,甚至不需要相互通信,通过以下阶段实现:
  1. Alice和Bob都不遵循任何约定。

  2. 由于手册和字节编译器鼓励使用黄色命名约定,所以当Alice和Bob开始遵循黄色约定时,就会出现这样的情况。

  3. Alice采用绿色约定,这至少可以保护她的代码免受其他人的黄色特殊变量干扰。

  4. Alice和Bob同时遵循这两种约定。

有关可能发生冲突的精确条件,请参见入侵特殊变量

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