Emacs之魂(四):标识符,符号和变量

  1. 云栖社区>
  2. 博客>
  3. 正文

Emacs之魂(四):标识符,符号和变量

何幻 2017-10-31 13:48:24 浏览1367
展开阅读全文

Emacs之魂(一):开篇
Emacs之魂(二):一分钟学会人界用法
Emacs之魂(三):列表,引用和求值策略
Emacs之魂(四):标识符,符号和变量
Emacs之魂(五):变量的“指针”语义
Emacs之魂(六):宏与元编程
Emacs之魂(七):变量捕获与卫生宏
Emacs之魂(八):反引用与嵌套反引用
Emacs之魂(九):读取器宏

    • -

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提供了两种定义变量的方式,defvarlet
其中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. 作用域和生存期

以上程序中,我们通过defvarlet,让a的值为字符串"1"b的值为字符串"2"
我们说,defvarlet建立了两个绑定(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-bufferx的值已经被定义了。

参考

GNU Emacs manual
GNU Emacs Lisp Reference Manual
Essentials of Programming Languages

网友评论

登录后评论
0/500
评论
何幻
+ 关注