1. 符号
上文我们提到了Emacs Lisp是一种Lisp-2,
即同一个符号(symbol)在不同的上下文中,可以分别表示两种不同的值(value):
变量(variable)或者函数(function),
这里符号(symbol)实际上是一个Lisp对象,而它的文本表示(textual representation)称之为标识符(identifier)。
标识符,符号和变量,这三个概念如果不谨慎对待,就会造成混乱。
其它编程语言可能没有“符号”的概念,这也是学习Lisp时容易困惑的原因之一。
此外,这里“符号”特指Lisp语言的“Symbol”,不能用汉语字面意思来理解它。
标识符,是Lisp的上下文无关文法(context-free grammar)中的一个非终结符(nonterminal),
它是一种词法结构,编译器前端(compiler front-end)在进行词法分析时会将标识符从字符流中识别出来。
符号(symbol)是一个Lisp对象,它是一个数据结构,由以下4个部分组成,
(1)name:symbol的名字
(2)value cell:作为一个动态变量,symbol的值
(3)function cell:作为一个函数,它的函数值
(4)property list:属性列表
标识符直接在Lisp代码中出现,会被读取为一个符号(symbol),
然后在不同的上下文中,Lisp求值器会看情况取出value cell或者function cell的内容,
作为该符号(symbol)的值(value)。
如果某一个函数接受符号(symbol)而不是它的值(value)作为参数,我们就得引用(quote)它,
即,我们使用引用,可以创建一个符号(symbol)字面量(literal)。
例如:symbol-name
函数可以用来获取符号(symbol)x
的name,
(symbol-name 'x)
"x"
结合上一篇,我们总结如下,
(1)直接写(foo bar bar)
表示函数调用或者宏调用
(2)加引用'(foo bar bar)
表示列表
(3)直接写x
表示变量或者函数
(4)加引用'x
表示符号(symbol)
如果只是这样的话,还很容易理解的,
可是value cell中只能保存动态变量,这一点理解起来就比较困难了。
“动态”是什么意思呢?还要从变量的定义和类别说起。
2. 全局变量和局部变量
Lisp提供了两种定义变量的方式,defvar
和let
,
其中defvar
用来定义全局变量,let
用来定义局部变量。
例子:
(defvar a "1")
(let ((b "2"))
(message "%s" b)) ; "2"
(message "%s" a) ; "1"
(message "%s" b) ; Error: Symbol’s value as variable is void: b
以上程序中,我们用defvar
定义了全局变量a
,和局部变量b
。
其中message
用于在Emacs的“echo area”中输出内容,
message
的第一个参数是表示格式的字符串,第二个参数是待输出的内容。
Lisp用分号表示注释。
为了执行这段程序,我们需要将它写到Emacs的buffer中,然后按M-x
再输入eval-buffer
回车,来求值整个缓冲区。
其中M-x
表示按住alt键,然后再按x,该快捷键命令会将光标定位到echo area,等待用户输入一个函数名,
我们输入函数eval-buffer
,它用来求值当前buffer,
它还有一个别名为ev-b
,可以记为M-x ev-b
。
注意,按M-x
之后,我们不用输入“M-x”,直接输入函数名“ev-b”就可以了。
程序最终的执行结果如注释所示,变量a
在整个程序中可用,而变量b
只在let
范围内可用。
3. 作用域和生存期
以上程序中,我们通过defvar
和let
,让a
的值为字符串"1"
,b
的值为字符串"2"
,
我们说,defvar
和let
建立了两个绑定(binding),将a
绑定为"1"
,b
绑定为"2"
。
The association between a variable and its value is called a binding.
——《Essentials of Programming Languages - P90》
变量除了可以分为全局变量和局部变量之外,还有另外两方面的属性,作用域(scope)和生存期(extent)。
作用域表示,在源代码文本中,绑定在什么地方(where)有效。
生存期表示,在程序执行的过程中,绑定在什么时候(when)有效。
Emacs Lisp支持两种形式的绑定,
动态绑定(dynamic binding)和静态绑定(lexical binding)。
动态绑定具有动态作用域和动态生存期,
动态作用域(dynamic scope),任何一段代码都可能访问变量的绑定,
动态生存期(dynamic extent),只有在绑定结构(例如let)执行的过程中,绑定才有效。
静态绑定具有静态作用域(也称词法作用域)和无限生存期,
词法作用域(lexical scope),绑定在绑定结构的源代码文本范围中有效,
无限生存期(indefinite extent),某些情况下,绑定可能永远有效。
幸运的是,Emacs Lisp同时支持这两种绑定方式,否则很难直观的理解它们,
默认情况下Emacs Lisp支持动态绑定,我们还可以为Emacs启用静态绑定规则。
3.1 动态绑定
例子:
(defvar x 0)
(defun getx ()
x)
(let ((x 1))
(getx)) ; 1
(getx) ; 0
其中defun
用于在Emacs Lisp中定义函数,以上代码定义了一个getx
无参数函数,
(getx)
是对该函数的调用。
在对getx
进行的第一次调用时,函数中引用了自由变量x
,Lisp要寻找程序执行期间对x
最近的绑定,
于是找到了let
表达式中,getx
调用之前对x
的绑定,为1
。
第二次调用getx
时,let
表达式的执行已经结束了,它对任何变量的绑定都将销毁,
这时候再调用getx
,程序执行期间最近的对x
的绑定,是(defvar x 0)
对x
的绑定,为0
。
在Emacs Lisp中,每一个符号(symbol)都有一个value cell,表示变量的当前值(current dynamic value),当一个符号(symbol)被给定一个局部绑定时(dynamic local binding),Emacs会把原来的value cell记录在一个栈上,然后把新值放入value cell中。当绑定结构(例如
let
)执行完后,Emacs进行弹栈操作,取出旧的值放回value cell中。
注意,其他语言中的全局变量并不是动态绑定,考虑以下JavaScript代码,
let x = 0;
function getx(){
return x;
}
((x)=>{
getx(); // 0
})(1);
getx(); // 0
JavaScript的全局变量仍然是静态绑定,第一个getx
被调用时,并不会携带x
的任何信息过去。
getx
总是从源代码文本范围内寻找x
,JavaScript对变量采用的是静态绑定。
3.2 静态绑定
例子:
; -*- lexical-binding: t -*-
(setq test (let ((foo "bar"))
(lambda ()
foo)))
(let ((foo "something-else"))
(funcall test)) ; "bar"
(funcall test) ; "bar"
其中,; -*- lexical-binding: t -*-
是Emacs的文件变量(file variable),
用于对当前文件或buffer启用静态绑定规则,它必须位于文件或者buffer的第一行。
在调用test
函数时,函数中引用的自由变量foo
,总是从源代码文本范围内离该函数最近的位置寻找,
于是找到了(lambda () foo)
外层let
中绑定的"bar"
,
所以两次对test
的调用,结果都是"bar"
。
在Emacs Lisp中,每一个绑定结构都会创建一个新的词法环境(lexical environment),在这个环境中,保存了变量名和它所对应值之间的对应关系(即,绑定关系),当Lisp求值器对某个符号(symbol)求值的时候,它首先从词法环境中寻找值,如果找到了,就用这个值。否则就认为这个符号(symbol)是一个动态变量,读取符号(symbol)的value cell作为变量的值。
4. 全局变量的动态性质
(1)动态绑定变量的值总是从符号(symbol)的value cell中获取,而静态绑定变量的值从词法环境中获取。
所以,无法使用symbol-value
获取静态绑定变量的值。
; -*- lexical-binding: t -*-
(let ((x 1))
(symbol-value 'x)) ; Symbol’s value as variable is void: x
(2)即使启用了变量的静态绑定规则,全局变量仍然是动态绑定的。
let
并没有引入新的静态变量x
,而是,建立了局部动态变量x
,然后用局部动态变量遮挡了全局动态变量的值。
; -*- lexical-binding: t -*-
(setq test (let ((x 1))
(lambda ()
x)))
(funcall test) ; 1
; -*- lexical-binding: t -*-
(defvar x 0)
(setq test (let ((x 1))
(lambda ()
x)))
(funcall test) ; 0
以上两段程序都启用了静态绑定规则,第一段程序中的x
是静态绑定的,
第二段程序中的x
是全局变量,使用defvar
定义了,所以它是动态绑定的。
在进行试验时,需要在全新的buffer中,分别测试,
否则(defvar x 0)
一旦执行,即使再重新M-x eval-buffer
,x
的值已经被定义了。
参考
GNU Emacs manual
GNU Emacs Lisp Reference Manual
Essentials of Programming Languages