美文网首页Lispemacs lisp语言介绍
[Emacs] Emacs之魂(四):标识符,符号和变量

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

作者: 何幻 | 来源:发表于2017-05-26 15:00 被阅读239次

    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

    相关文章

      网友评论

        本文标题:[Emacs] Emacs之魂(四):标识符,符号和变量

        本文链接:https://www.haomeiwen.com/subject/aoznxxtx.html