🌳 理解Emacs lexical binding 和 dynamic binding
两个程序例子
计数器程序
这里故意用了同名的 test/counter 变量作为全局和局部来说明词法绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;; -*- lexical-binding: t -*- ;;第一行
(setq test/counter 0)
(defun test/add-2-lambda ()
(let ((test/counter 0))
(lambda ()
(message "inner test/counter is current %d" test/counter)
(setq test/counter (+ test/counter 2))
)))
(setq test/add-2-fun (test/add-2-lambda))
(funcall test/add-2-fun)
(funcall test/add-2-fun)
(setq test/counter 0)
(funcall test/add-2-fun)
(funcall test/add-2-fun)
(message "outer test/counter is current %d" test/counter)
假如没有第一行的 lexical 绑定, Emacs中默认是动态绑定
那么结果是:
1
2
3
4
5
6
inner test/counter is current 0
inner test/counter is current 2
inner test/counter is current 0
inner test/counter is current 2
outer test/counter is current 4
可以看到,没有词法绑定的话, test/add-2-lambda闭包中的 test/counter 变量其实就是访问的外部的 test/counter
有了lexical词法绑定,运行结果是
1
2
3
4
5
6
inner test/counter is current 0
inner test/counter is current 2
inner test/counter is current 4
inner test/counter is current 6
outer test/counter is current 0
因为开启了 lexical-binding: t,test/add-2-lambda 中的 let 创建了一个严格受限于文本结构的局部词法环境。内部的 lambda 在求值时,将这个局部环境打包成了一个闭包(Closure)。因此,无论你在外部如何使用 (setq test/counter 0) 去修改全局变量,或者无论你在哪里调用 funcall,闭包内部修改的始终是当年被捕获的那个私有的 test/counter,全局的 test/counter 不会受到任何影响
可以改进实验:测试闭包环境的“多实例独立性”
1
2
3
4
5
(defun make-counter (init-val)
(let ((counter init-val))
(lambda ()
(setq counter (+ counter 2))
(message "Counter is now %d" counter))))
定义的位置决定了看到的值
1
2
3
4
5
6
7
8
9
10
11
12
;; -*- lexical-binding: t -*-
(setq x 0)
(defun getx ()
(message "x is %d" x)
x)
(setq x 3)
(let ((x 2))
(getx))
定义时的级别决定了可见性:你的 getx 函数是在代码的最外层(即顶层、全局级别)定义的。由于它不是在任何 let 等局部绑定结构的内部定义的,因此它的词法环境就是全局环境。它无法“看见”在它外部调用的那些局部变量。
文本范围(Textual Scope)的严格隔离:代码中的 let 表达式创建了一个局部词法环境(绑定了 x = 2)。在词法绑定下,这个 x = 2 仅仅在 let 结构内部的字面文本范围内有效。
运行时的查找机制:当在 let 内部调用 (getx) 时,getx 开始执行。Lisp 解释器在遇到变量 x 时,会首先在 getx 被定义时的词法环境中寻找。因为 getx 是在 let 的外部定义的,它找不到这个局部的词法绑定,于是它就会去全局环境(即符号的 value cell)里获取动态的全局值。
最终的结果:在你调用 let 之前,执行过一句 (setq x 3),这修改了全局的 x。所以当 getx 去全局环境里找 x 时,它会找到 3。最后这个程序会打印并返回 3。
词法绑定到底是什么?
在 Emacs Lisp 传统的动态绑定下,当你使用 let 创建一个局部变量时,这个绑定在 let 表达式执行期间会一直存在于一个全局的“栈”的顶部。这意味着如果你在 let 内部调用了在其他文件中定义的函数,那个外部函数也能看到并修改你的局部变量。 而词法绑定严格限制了变量的作用域:对变量的任何引用都必须位于创建该绑定的代码的文本范围(textual scope)之内
这保证了在 let 内部调用的外部函数绝对无法“看”到你的局部变量 ,从而使得程序更加健壮,更不容易出错
那动态绑定和java,c/c++中的静态变量有什么区别?
java等语言中的静态变量,修改是破坏性的,不可逆的
Emacs中的动态绑定是维护一个一直存在于一个全局的“栈”的顶部,也就是同名变量如果在其他函数中修改,那么会push一个新的值到这个栈顶,函数结束,那么这个值pop出去,这时候有其他访问的话,访问的旧值