Common Lisp中的动态和词法变量

时间:2009-01-20 22:53:23

标签: lisp common-lisp lexical-scope

我正在读Peter Seibel撰写的“Practical Common Lisp”一书。

在第6章“变量”部分 “Lexical Variables and Closures”和“Dynamic,a.k.a. Special,Variables”。 http://www.gigamonkeys.com/book/variables.html

我的问题是两个部分中的示例都显示了(让...)如何影响全局变量,并没有真正区分动态和词汇变量之间的区别。

我理解闭包是如何工作的,但我真的不知道在这个例子中让我们如此特别:

(defvar *x* 10)

(defun foo ()
  (format t "Before assignment~18tX: ~d~%" *x*)
  (setf *x* (+ 1 *x*))
  (format t "After assignment~18tX: ~d~%" *x*))


(defun bar ()
  (foo)
  (let ((*x* 20)) (foo))
  (foo))


CL-USER> (foo)
Before assignment X: 10
After assignment  X: 11
NIL


CL-USER> (bar)
Before assignment X: 11
After assignment  X: 12
Before assignment X: 20
After assignment  X: 21
Before assignment X: 12
After assignment  X: 13
NIL

我觉得这里没有什么特别之处。 bar 中的外部 foo 会增加所包围的全局 x foo bar 中增加阴影 x 。有什么大不了的?我不认为这应该如何解释词汇和动态变量之间的区别。然而,这本书继续这样:

  

那么这是如何工作的?怎么LET   知道当它绑定 x 时   应该创建一个动态绑定   而不是正常的词汇绑定?   它知道因为名字已经存在   声明特别.12每个人的名字   用DEFVAR定义的变量   自动声明DEFPARAMETER   全球特别。

如果使用“正常的词法绑定”绑定 x ,会发生什么?总而言之,动态绑定和词法绑定之间有什么区别?这个例子对于动态绑定有何特别之处?

5 个答案:

答案 0 :(得分:47)

发生了什么事?

你说:觉得这里没有什么特别之处。 foo中的bar外的x会增加全局fooletbar所包围的x会增加阴影LET。有什么大不了的?

此处发生的特殊*x* 可以遮蔽*x*的值。用词汇变量是不可能的。

代码通过DEFVAR声明FOO特殊

现在在*x*中,FOO的值被动态查找。 *x*将采用*x*的当前动态绑定,如果没有,则采用符号LET的符号值。例如,可以使用LET引入新的动态绑定

另一方面,词汇变量必须出现在某个词汇环境中。 LAMBDADEFUNx和其他人可以引入此类词汇变量。请参阅此处以三种不同方式介绍的词汇变量(let ((x 3)) (* (sin x) (cos x))) (lambda (x) (* (sin x) (cos x))) (defun baz (x) (* (sin x) (cos x)))

(defvar x 0)

(let ((x 3))
  (* (sin x) (cos x)))

(lambda (x)
  (* (sin x) (cos x)))

(defun baz (x)
  (* (sin x) (cos x)))

如果我们的代码是:

X

然后DEFVAR在上述所有三种情况下都是特殊,因为X声明声明*X*特殊 - 全球所有级别。因此,有一种惯例是将特殊变量声明为(defun bar () (foo) (let ((*x* 20)) (foo)) (foo)) 。因此,只有周围有恒星的变量才能特殊 - 通过约定。这是一个有用的惯例。

在您的代码中,您有:

*x*

由于DEFVAR已通过代码中的LET声明为特殊,因此*x*构造引入了新的动态绑定 FOO。然后调用FOO。由于*x**x*使用动态绑定,它会查找当前的{em}动态绑定,并发现20动态绑定到special

特殊变量的值可在当前动态绑定中找到。

本地特殊声明

还有本地(defun foo-s () (declare (special *x*)) (+ *x* 1)) 声明:

DEFVAR

如果变量已被DEFPARAMETERspecial声明为特殊,则可省略本地(defun foo-l (x) (+ x 1)) 声明。

词法变量直接引用变量绑定:

(let ((f (let ((x 10))
           (lambda ()
             (setq x (+ x 1))))))
  (print (funcall f))    ; form 1
  (let ((x 20))          ; form 2
    (print (funcall f))))

让我们在实践中看到它:

LET

这里所有变量都是词汇。在表单2 中,X不会影响函数f中的LET ((X 10)。它不能。该函数使用由X引入的词法绑定变量。使用表单2 中的另一个词法绑定(let ((f (let ((x 10)) (declare (special x)) (lambda () (setq x (+ x 1)))))) (print (funcall f)) ; form 1 (let ((x 20)) ; form 2 (declare (special x)) (print (funcall f)))) 围绕调用对我们的函数没有影响。

让我们试试特殊的变量:

X

现在怎么办?这有用吗?

它没有!

第一个表单调用该函数,它会尝试查找X的动态值,但没有。我们在表单1 中收到错误:LET未绑定,因为没有动态绑定生效。

表单2 可行,因为带有special声明的X会为{{1}}引入动态绑定。

答案 1 :(得分:25)

当变量词法范围时,系统会查找函数已定义的位置,以查找自由变量的值。当变量动态范围时,系统会查看函数的名称以查找自由变量的值。 Common Lisp中的变量默认都是词法;但是,可以使用 defvar defparameter 在顶层定义动态范围的变量。

更简单的例子

词法范围(使用setq):

(setq x 3)

(defun foo () x)

(let ((x 4)) (foo)) ; returns 3

动态范围(使用defvar):

(defvar x 3)

(defun foo () x)

(let ((x 4)) (foo)) ; returns 4

如何知道变量是词法变量还是动态变量? 它没有。另一方面,当foo找到X的值时,它将首先找到在顶层定义的词法值。然后检查该变量是否应该是动态的。如果是,那么foo会查看调用环境,在这种情况下,使用let将X的值掩盖为4。

(注意:这是过于简单化,但它有助于可视化不同范围规则之间的差异)

答案 2 :(得分:9)

也许这个例子会有所帮助。

;; the lexical version

(let ((x 10)) 
  (defun lex-foo ()
    (format t "Before assignment~18tX: ~d~%" x)
    (setf x (+ 1 x))
    (format t "After assignment~18tX: ~d~%" x)))

(defun lex-bar ()
  (lex-foo)
  (let ((x 20)) ;; does not do anything
    (lex-foo))
  (lex-foo))

;; CL-USER> (lex-bar)
;; Before assignment X: 10
;; After assignment  X: 11
;; Before assignment X: 11
;; After assignment  X: 12
;; Before assignment X: 12
;; After assignment  X: 13

;; the dynamic version

(defvar *x* 10)
(defun dyn-foo ()
  (format t "Before assignment~18tX: ~d~%" *x*)
  (setf *x* (+ 1 *x*))
  (format t "After assignment~18tX: ~d~%" *x*))

(defun dyn-bar()
  (dyn-foo)
  (let ((*x* 20))
    (dyn-foo))
  (dyn-foo))

;; CL-USER> (dyn-bar)
;; Before assignment X: 10
;; After assignment  X: 11
;; Before assignment X: 20
;; After assignment  X: 21
;; Before assignment X: 11
;; After assignment  X: 12

;; the special version

(defun special-foo ()
  (declare (special *y*))
  (format t "Before assignment~18tX: ~d~%" *y*)
  (setf *y* (+ 1 *y*))
  (format t "After assignment~18tX: ~d~%" *y*))

(defun special-bar ()
  (let ((*y* 10))
    (declare (special *y*))
    (special-foo)
    (let ((*y* 20))
      (declare (special *y*))
      (special-foo))
    (special-foo)))

;; CL-USER> (special-bar)
;; Before assignment X: 10
;; After assignment  X: 11
;; Before assignment X: 20
;; After assignment  X: 21
;; Before assignment X: 11
;; After assignment  X: 12

答案 3 :(得分:7)

你也可以告诉你的Lisp动态绑定局部变量:

(let ((dyn 5))
  (declare (special dyn))
  ... ;; DYN has dynamic scope for the duration of the body
  )

答案 4 :(得分:6)

从PCL重写示例。

;;; Common Lisp is lexically scoped by default.

λ (setq x 10)
=> 10

λ (defun foo ()
    (setf x (1+ x)))
=> FOO

λ (foo)
=> 11

λ (let ((x 20))
    (foo))
=> 12

λ (proclaim '(special x))
=> NIL

λ (let ((x 20))
    (foo))
=> 21

来自On Lisp的另一个很好的解释,第2.5章范围:

Common Lisp是一个词汇范围的Lisp。方案是最古老的方言,具有词汇范围;在Scheme之前,动态范围被认为是Lisp的定义特征之一。

词法和动态范围之间的区别归结为实现如何处理自由变量。如果符号通过作为参数出现,或者通过变量绑定运算符(如let和do)建立为变量,则符号在表达式中绑定。没有绑定的符号被认为是免费的。在此示例中,范围发挥作用:

(let ((y 7)) 
  (defun scope-test (x)
  (list x y)))

在defun表达式中,x被绑定,y是自由的。自由变量很有意思,因为它们的值应该是什么并不明显。关于绑定变量的值没有不确定性 - 当调用scope-test时,x的值应该是作为参数传递的任何值。但是y的价值应该是多少?这是方言范围规则所回答的问题。

在动态范围的Lisp中,为了在执行范围测试时找到自由变量的值,我们回顾一下调用它的函数链。当我们找到y绑定的环境时,y的绑定将是范围测试中使用的绑定。如果我们找不到,我们取y的全局值。因此,在动态范围的Lisp中,y将具有它在调用表达式中具有的值:

> (let ((y 5)) (scope-test 3))
    (3 5)

对于动态范围,当定义范围测试时,它意味着y不会绑定到7。重要的是,当调用范围测试时y的值为5。

在词法范围内的Lisp中,我们不是回顾一下调用函数链,而是回顾定义函数时的包含环境。在词法范围的Lisp中,我们的示例将捕获定义范围测试的y的绑定。所以这就是Common Lisp会发生的事情:

> (let ((y 5)) (scope-test 3))
    (3 7)

这里,在调用时y与5的绑定对返回值没有影响。

虽然你仍然可以通过声明一个特殊的变量来获得动态范围,但是词法范围是Common Lisp中的默认值。总的来说,Lisp社区似乎很少看到动态范围的传递。一方面,它曾经导致可怕的难以捉摸的错误。但词法范围不仅仅是一种避免错误的方法。正如下一节所示,它也使一些新的编程技术成为可能。