如何解释词法绑定与动态绑定?

How to Explain Lexical vs Dynamic Binding?

我阅读了有关绑定的相关 post,但仍有疑问。

以下是我找到的例子。谁能告诉我结论是否正确?

(i) 中 x 的动态绑定:

(defun j ()
 (let ((x 1))
    (i)))

(defun i ()
   (+ x x))

> (j)
2

i2 中 x 的词法绑定:

(defun i2 (x)
       (+ x x))

(defun k ()
   (let ((x 1))
       (i2 2)))

> (k)
4

ANSI CL 中没有全局词法变量,因此执行动态绑定:

(setq x 3)

(defun z () x)

> (let ((x 4)) (z))
4

动态绑定,似乎绑定到词法范围的变量:

(defvar x 1)
(defun f (x) (g 2))
(defun g (y) (+ x y))
> (f 5)
7

基于以上测试,CL首先尝试词法绑定。如果环境中没有词法匹配,则 CL 尝试动态绑定。似乎任何以前的词法范围变量都可用于动态绑定。这个对吗?如果不是,行为是什么?

总而言之:不,CL 从不 'tries one kind of binding then another':而是它在编译时确定哪种绑定有效,并引用它。此外,变量绑定和引用始终是词法的,除非有特殊的有效声明,在这种情况下它们是动态的。唯一一次 词法 不明显是否存在有效的特殊声明是全局特殊声明,通常通过 defvar / defparameter 执行,这可能不可见(例如它们可能在其他源文件中)。

它决定像这样绑定有两个很好的理由:

  • 首先,这意味着阅读代码的人可以(可能在不可见的全局特殊声明的情况下除外)知道正在使用哪种绑定 - 对变量引用是否指向感到惊讶该变量的动态或词法绑定很少是一种愉快的体验;
  • 其次,这意味着良好的编译是可能的——除非 编译器 可以知道变量的绑定类型是什么,否则它永远无法编译引用该变量的良好代码,而这对于具有一定范围的词法绑定尤其如此,其中变量引用通常可以完全编译掉。

重要的一点:始终使用视觉上不同的方式来编写具有全局特殊声明的变量。所以永远不要说 (defvar x ...) 之类的话:语言并不禁止这样做,但它在阅读代码时只会造成灾难性的误导,因为在这种情况下,特殊声明通常 可见给阅读代码的人。此外,将 *...* 用于全局特价,例如语言定义的全局特价,就像其他人一样。我经常将 %...% 用于 nonglobal 特价,但据我所知没有最佳实践。 常量 可以使用朴素的名称(语言定义了很多示例),因为它们可能不受约束。

下面的详细示例和答案假定:

  • Common Lisp(不是任何其他 Lisp);
  • 引用的代码是所有代码,因此没有额外的声明或类似的东西。

这是一个全局特殊声明生效的例子:

;;; Declare *X* globally special, but establish no top-level binding
;;; for it
;;;
(defvar *x*)

(defun foo ()
  ;; FOO refers to the dynamic binding of *X*
  *x*)

;;; Call FOO with no binding for *X*: this will signal an
;;; UNBOUND-VARIABLE error, which we catch and report
;;;
(handler-case
    (foo)
  (unbound-variable (u)
    (format *error-output* "~&~S unbound in FOO~%"
            (cell-error-name u))))

(defun bar (x)
  ;; X is lexical in BAR
  (let ((*x* x))
    ;; *X* is special, so calling FOO will now be OK
    (foo)))

;;; This call will therefore return 3
;;;
(bar 3)

这是一个包含非全局特殊声明的示例。

(defun foo (x)
  ;; X is lexical
  (let ((%y% x))
    (declare (special %y%))
    ;; The binding of %Y% here is special.  This means that the
    ;; compiler can't know if it is referenced so there will be no
    ;; compiler message even though it is unreferenced in FOO.
    (bar)))

(defun bar ()
  (let ((%y% 1))
    ;; There is no special declaration in effect here for %Y%, so this
    ;; binding of %Y% is lexical.  Therefore it is also unused, and
    ;; tere will likely be a compiler message about this.
    (fog)))

(defun fog ()
  ;; FOG refers to the dynamic binding of %Y%.  Therefore there is no
  ;; compiler message even though there is no apparent binding of it
  ;; at compile time nor gobal special declaration.
  (declare (special %y%))
  %y%)

;;; This returns 3
;;;
(foo 3)

请注意,在这个例子中,词汇上总是显而易见的什么绑定应该对 %y% 有效:只看函数本身就会告诉你你想要什么需要知道。


下面是对您的示例代码片段的评论。

(defun j ()
 (let ((x 1))
   (i)))

(defun i ()
  (+ x x))

> (j)
<error>

这在 CL 中是非法的:x 未绑定在 i 中,因此对 i 的调用应该发出错误信号(特别是 unbound-variable 错误)。

(defun i2 (x)
  (+ x x))

(defun k ()
  (let ((x 1))
    (i2 2)))

> (k)
4

这没问题:i2 在词法上绑定 x,因此永远不会使用 k 建立的绑定。您可能会收到有关 k 中未使用变量的编译器警告,但这当然取决于实现。

(setq x 3)

(defun z () x)

> (let ((x 4)) (z))
<undefined>

这是 CL 中的未定义行为:setq 的可移植行为是改变 existing 绑定而不是 create 一个新的绑定。您正在尝试使用它来执行后者,这是未定义的行为。许多实现允许 setq 在顶层像这样使用,它们可以创建本质上是全局词法、全局特殊的东西,或者做一些其他事情。虽然这通常在与给定实现的顶层交互时在实践中完成,但这并不能使其成为语言中定义的行为:程序永远不应该这样做。当您执行此操作时,我自己的实现会从隐藏的喷嘴向程序员的大致方向喷出白热的铅柱。

(defvar x 1)
(defun f (x) (g 2))
(defun g (y) (+ x y))
> (f 5)
7

这是合法的。这里:

  • defvar 声明 x 全局特殊,因此 x 的所有绑定都是动态的,并建立了 x 到 [=36= 的顶级绑定];
  • f 中,其参数的绑定 x 因此将是动态的,而不是词法的(没有前面的 defvar 它将是词法的);
  • g 中,对 x 的自由引用将是对其动态绑定(没有前面的 defvar 它将是编译时警告(依赖于实现)和运行-时间错误(与实现无关)。
(defun j ()
 (let ((x 1))
    (i)))

(defun i ()
   (+ x x))

> (j)
2

这实际上是 Common Lisp 中的未定义行为。使用未定义变量(此处在函数 i 中)的确切后果未在标准中定义。

CL-USER 75 > (defun j ()
               (let ((x 1))
                 (i)))
J

CL-USER 76 > (defun i ()
               (+ x x))
I

CL-USER 77 > (j)

Error: The variable X is unbound.
  1 (continue) Try evaluating X again.
  2 Return the value of :X instead.
  3 Specify a value to use this time instead of evaluating X.
  4 Specify a value to set X to.
  5 (abort) Return to top loop level 0.

Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.

CL-USER 78 : 1 > 

如您所见,Lisp 解释器 (!) 在运行时会报错。

现在:

(setq x 3)

SETQ 设置一个未定义的变量。这在标准中也没有完全定义。大多数编译器会抱怨:

LispWorks

;;;*** Warning in (TOP-LEVEL-FORM 1): X assumed special in SETQ
; (TOP-LEVEL-FORM 1)
;; Processing Cross Reference Information
;;; Compilation finished with 1 warning, 0 errors, 0 notes.

或SBCL

; in: SETQ X
;     (SETQ X 1)
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::X
; 
; compilation unit finished
;   Undefined variable:
;     X
;   caught 1 WARNING condition


(defvar x 1)
(defun f (x) (g 2))
(defun g (y) (+ x y))
> (f 5)
7

Dynamic Binding, which appears to bind to a lexically scoped variable

不,x全局DEFVAR 定义为特殊的。因此 fx 创建了一个动态绑定,并在动态环境中查找函数 gx 的值。

开发人员基本规则

  • 永远不要使用未定义的变量
  • 当使用特殊变量时,总是将 * 放在它们周围,以便在使用它们时始终可见,正在使用动态绑定和查找。这也确保了 NOT 不小心将全局变量声明为 special。一个 (defvar x 42)x 将永远是一个 special 使用动态绑定的变量。这通常不是我们想要的,它可能会导致难以调试错误。