47

我正在阅读 Peter Seibel 的《实用通用 Lisp》一书。

在第 6 章,“变量”部分,“词法变量和闭包”和“动态,又名特殊,变量”。 http://www.gigamonkeys.com/book/variables.html

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

我了解闭包是如何工作的,但在这个例子中我真的不明白 let 有什么特别之处:

(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,而被let in bar包围的foo递增阴影x。有什么大不了的?我不明白这应该如何解释词法变量和动态变量之间的区别。然而这本书仍然是这样的:

那么这是如何工作的呢?LET 怎么知道当它绑定x时它应该创建一个动态绑定而不是正常的词法绑定?它知道是因为名称已被声明为特殊的。12 使用 DEFVAR 和 DEFPARAMETER 定义的每个变量的名称都会自动声明为全局特殊的。

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

4

5 回答 5

59

这是怎么回事?

你说:感觉这里没什么特别的。外部fooinbar递增 global x,并由in递增 shadowedfoo包围。有什么大不了的?letbarx

这里发生的特殊情况是LET 可以影响*x*. 使用词法变量是不可能的。

该代码*x*通过. _DEFVAR

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

另一方面,词法变量必须存在于词法环境中的某个地方。LET, LAMBDA,DEFUN和其他人可以引入这样的词法变量。x请参阅此处以三种不同方式引入的词法变量:

(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*一个新的动态绑定。然后被调用。由于在内部使用动态绑定,它查找当前的并发现它是动态绑定的。DEFVARLET*x*FOOFOO*x**x*20

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

本地特殊声明

还有本地special声明:

(defun foo-s ()
  (declare (special *x*))
  (+ *x* 1))

如果变量已由a或声明为特殊变量,则可以省略局部声明。DEFVARDEFPARAMETERspecial

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

(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))))

这里所有的变量都是词法的。在表格 2中,LET不会影响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但没有。我们在表单 1中得到一个错误:X未绑定,因为没有动态绑定生效。

形式 2会起作用,因为LETwithspecial声明引入了X.

于 2010-03-05T10:40:07.157 回答
24

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

一个更简单的例子

词法作用域(使用 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

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

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

于 2009-01-21T01:32:35.750 回答
10

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

;; 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
于 2009-02-12T21:24:17.830 回答
9

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

(let ((dyn 5))
  (declare (special dyn))
  ... ;; DYN has dynamic scope for the duration of the body
  )
于 2009-02-13T12:43:46.437 回答
7

从 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是最古老的具有词法范围的方言;在 Scheme 之前,动态范围被认为是 Lisp 的定义特性之一。

词法作用域和动态作用域之间的区别归结为实现如何处理自由变量。如果符号已被建立为变量,则符号在表达式中被绑定,无论是通过作为参数出现,还是通过变量绑定运算符(如 let 和 do)。不受约束的符号被称为是自由的。在这个例子中,作用域开始发挥作用:

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

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

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

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

对于动态范围,在定义范围测试时 y 绑定到 7 没有任何意义。重要的是调用 scope-test 时 y 的值为 5。

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

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

这里调用时将 y 绑定到 5 对返回值没有影响。

尽管您仍然可以通过将变量声明为特殊来获得动态范围,但词法范围是 Common Lisp 中的默认设置。总的来说,Lisp 社区似乎对动态作用域的消逝没有什么遗憾。一方面,它曾经导致可怕的难以捉摸的错误。但是词法作用域不仅仅是一种避免错误的方法。正如下一节将展示的,它还使一些新的编程技术成为可能。

于 2012-09-22T06:30:34.897 回答