Common Lisp中的变量

介绍Common Lisp中的变量是如何绑定的,以及什么是Lisp–1和Lisp–2

Lisp–1 vs Lisp–21

Scheme的求值模型非常简单:只是用一个名字空间,所有表达式中相应位置的值应该是明确的。

Common Lisp和Scheme最大的不同是,函数名字空间和数据的名字空间是分离的。操作函数名字空间的语句包括,defun,flet,labels,defmethod和defgeneric等方法。要用函数名作为另一个函数的参数来传递函数时,需要使用function特殊操作符或者使用#’ 。

我们称Scheme的变量模型为Lisp–1,Common Lisp的变量模型是Lisp–2。

绑定

在编程语言中绑定是变量名(变量标识符)和对象(保存于内存中的存储单元,数据或代码)的映射关系。在这个绑定过程中是作用域有密不可分的关系,作用域决定了哪个变量绑定了哪个存储单元。 为变量建立绑定之后,就可以通过变量名来引用其所绑定的值。绑定的具体含义,可以参考下图

该图可认为是表达式 (setf S 10) 执行结果,其中有两个重要的步骤。

  1. 把符号 S 跟内存中的一存储单元建立起绑定。之后,符号 S 出现的地方就代表(引用)了这一存储单元。
  2. 把符号 S 所引用的存储单元的值设为整数10。

Common Lisp中所有的值,至少从概念上讲,都是对对象的引用(references)。将一个变量赋予新值只会改变此变量将引用哪个对象,对其之前引用的对象没有影响。 但是,如果一个变量保存了对一个可变对象的引用,你可以用这个引用来修改此对象,而且这种改动将作用于其他引用这个相同对象的代码。

词法作用域(lexical scoping)

词法作用域又叫静态作用域(static scope)。顾名思义,词法变量即是使用词法作用域的变量。 在词法作用域里,一个变量的变量名只能在一个函数或一段代码区域( block )内存在,此时变量名才会绑定到变量的值。

词法变量拥有不确定的生存期,即从时间上来讲,一个词法变量可以在任意的时间里持续存在,取决于该变量需要被使用(reference)多久。 词法作用域里,对于函数体中的一个符号,不会逐层检查函数的调用链,而是检查函数定义时的外部环境,即捕捉的是函数定义时该符号的绑定。

动态作用域(dynamic scoping)

使用动态作用域的变量叫做动态(dynamic)变量,有时也叫做特殊(special)变量。动态作用域里,每个变量名(变量标识符)都拥有一个全局的绑定栈。 引入一个与动态变量同名的局部变量会为此变量名创建一个新的变量绑定并将其压入此变量名的全局绑定栈中,一个全局的变量名(变量标识符)总是引用当前其栈顶的绑定, 当使用该变量绑定的代码执行完毕(即程序控制流离开了此变量的作用域),该变量绑定就会从此变量名的全局绑定栈中被弹出,该变量绑定就失效。

动态作用域表示的范围是不确定的,可从任何位置访问一个动态变量,取决于它们在什么地方被绑定。动态变量拥有动态的生存期。因容易引起误会而需要注意的是,不确定的作用域和动态生存期的组合经常被错误地称为动态作用域(dynamic scope)。

动态作用域里,函数执行遇到一个符号,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。最外层的绑定即是全局状态下的那个值。

Common Lisp的例子

请看下面的代码

1
2
3
4
5
6
7
(let ((y 7))
  (defun foo (x)
    (print x)
    (print y)))

(let ((y 5))
  (foo 1))

我们通过SLIM执行后,可以得到输出1和7,这说明Common Lisp使用的是词法作用域。在foo中寻找y的绑定时,它检查函数foo的词法上下文。

再请看下面代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(let ((y 7))
  (defun foo (x)
    (print x)
    (print y)
    (setq y (+ y 2))))

(let ((y 5))
  (foo 1)
  (foo 1))

(let ((y 5))
  (foo 2))
  

我们通过SLIM执行后,我们会看到,1,7,1,9,2和11。在例子中的第一个let表里,定义了一个变量,符号名为y并绑定了值7,那么这个y的作用域就是这个let表区域。 foo函数定义在这个区域内,其内部会使用到一个符号名为y的变量。 那么在词法作用域的情况下,当foo被调用时,其会查找其定义的环境有没有符号名y的变量可以绑定, 如果有则把foo中符号y的值绑定,在这里就是7。 并且这里foo中的y和外部let中的y共享一个值,都是对这个值的引用,并不是拷贝了一个新值。

Common Lisp的动态作用域

如果想让Common Lisp中某个变量具备动态作用域该怎么办呢?那么我们就需要使用special函数。

它的作用就是

    指定相应的变量名称是动态绑定的,此操作会影响变量绑定同时也会影响变量引用。
    受影响的所有变量绑定都是动态绑定,受影响的变量引用是指当前的动态绑定。

我们只需要对前面的代码,稍作修改就可以让他具有动态作用域。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(let ((y 7))
  (defun foo (x)
    (declare (special y))
    (print x)
    (print y)))

(let ((y 5))
  (declare (special y))
  (foo 1))
  

混合使用词法和动态作用域

有些时候,我们需要混合使用词法作用域和动态作用域。请看下面代码:

1
2
3
4
5
6
7
8
9
(let ((a 1))
  (defun foo ()
    (if (boundp 'a)
        (locally (declare (special a)) a)
        a)))
(foo)
(let ((a 2))
   (declare (special a))
   (foo))

我们可以清晰的看到,我们声明foo的时候默认值给的是1,当我们声明特殊变量使用动态作用域它就会输出2。这里面的细节就涉及到Common Lisp对变量绑定的定义了。

参考

1Common Lisp - The Function NameSpace