动态作用域的优点是什么?

32
我了解到静态作用域是做事情的唯一明智选择,而动态作用域只是由于解释器/编译器实现不佳而导致的工具,是魔鬼的产物。
然后我在 Common Lisp vs. Scheme 文章中看到这个代码片段:
    既具有词法作用域,也具有动态作用域         标准仅支持词法作用域。
    特殊变量。Common Lisp的                动态作用域变量由一些实现提供
    在这一点上胜出。                                 作为扩展,但使用它们的代码不具备可移植性。
(我听说过关于动态作用域是否是个好主意的争论。我不在乎。我只是指出你可以用它做一些不容易做到的事情。)
为什么Common Lisp“在这一点上胜出”?使用动态作用域更容易做什么?我真的无法证明它是必要的/它是一个好事。

https://dev59.com/V07Sa4cB1Zd3GeqP1z6x#2979455 - martynas
10个回答

41

和其他一切一样,动态作用域仅仅是一种工具。如果使用得当,可以使某些任务更容易完成。如果使用不好,可能会引入错误和头疼问题。

我确实可以看到它的一些用途。可以消除将变量传递给某些函数的需求。

例如,我可能在程序开头设置显示,并且每个图形操作都假定这个显示。

如果我想在该显示内设置一个窗口,则可以将该窗口“添加”到除了指定显示外的变量堆栈中,在这种状态下执行的任何图形操作都将转到窗口而不是整个显示。

这是一个刻意制造的例子,同样可以通过将参数传递给函数来很好地完成,但当您查看某些代码生成的任务时,就会意识到全局变量确实是一种更容易的方式,而动态作用域为您提供了全局变量的清晰性和函数参数的灵活性

-Adam


1
刚刚收到了一个踩 - 如果我的回答不正确或需要改进,欢迎留下评论。 - Adam Davis
4
我不知道为什么会给它点踩,可能是一个意外。现在看来,我唯一不喜欢的是“xx只是一种工具”这种说法有些陈词滥调,同时,您(我想您是无意中)在最后一句中使用了“dynamic typing”,而实际上您应该使用“dynamic scoping”。除此之外,答案还不错。我已经取消了点踩。 - Max Strini
对我来说,这还不足以让我踩一下,但“全局变量的理智”是一个让我考虑踩的短语。有时候全局变量更好,但当所有能够看到或更改全局变量的代码无法一次适应人类思维,或需要花费相当大的努力才能找到时,它们就会变得非常糟糕。一旦你无法一次性看到整个程序,该程序中的全局变量就会产生“幽灵般的远程作用”,如果可变,则所有你无法记住的代码都可能通过该全局变量相互关联。 - mtraceur
所以我认为“理智”不是我通常会归因于全局变量的东西,即使在没有这些问题的良好结构化代码中,我也不认为通过代码进行隐形信息传递相对于在整个代码中明确表达信息依赖关系是“理智”的。 (话虽如此,我确实认为您正在尝试表达一些有见地的东西,我只是试图清晰地区分隐式传递参数何时/如何比显式传递所有内容更好而不是更糟。) - mtraceur
我认为关于“理智”所指的好处的一个很好的例子是日志记录:日志记录深入嵌入在调用树中,但通常需要来自高层的大量参数化。整个程序的用户或开发人员需要能够决定程序记录什么,但各种库需要告诉日志记录系统要记录什么信息以及具有何种相关性/严重性。同时,一些位于中间的代码可能最好完全不涉及日志记录——它没有任何记录,并且在不传递日志记录参数的情况下更清晰。 - mtraceur

19
动态作用域的主要风险是意外后果。动态作用域使范围遵循运行时堆栈,这意味着在任何符号使用点处,作用域中的符号集合要大得多,而且很不明显。动态作用域变量很像全局变量,只是可能有多个版本的每个变量,只有最新的定义可见,隐藏了所有其他版本。
动态作用域在实践中有用的地方是需要对运行时堆栈敏感的行为。例如(一般而言,不特定于Lisp或其变体):
- 异常处理——当异常发生时,最顶层的catch块是“在作用域”内的。 - 安全性——基于.NET代码的安全性根据调用它的代码决定某些特权API的可访问性。
依赖它进行其他用途的问题在于它会在词法上相距较远的代码之间创建隐式依赖关系和耦合。从这个角度来看,它也类似于全局变量,只是可能更糟糕(由于被动态覆盖的定义)。

10

动态作用域在某些领域特定语言中非常有用。特别是在样式表语言中,它可以方便地使用。我的经验来自于GNU TeXmacs 样式表语言。

在这种语言中,显示参数存储在动态作用域变量中。这些变量会影响其作用域内的每个原子的呈现效果,包括通过在作用域内调用的函数生成的原子。

TeXmacs 中的动态作用域还用于标记交叉引用。用于交叉引用的锚点从其环境获取其标签。例如,包含在公式块中的锚点将使用公式编号作为标签,而不是在公式之后的锚点使用章节编号。

想想看,Unix 环境变量也是动态作用域变量。尽管内部作用域不能更改外部作用域变量的值。

正如Barry Kelly 指出的那样,动态作用域也可以用于实现关注调用范围的语言特性,例如异常处理或上下文相关的权限处理。在存在连续性的情况下,可以进入和退出作用域而无需遍历调用堆栈。


7
动态作用域允许定义上下文函数。在这方面,它与现代框架中的依赖注入非常相似。(例如,考虑当您使用依赖注入定义注释Java类以允许透明初始化各种引用时。(cf spring或JPA等))显然,动态作用域对于给定函数的调用站点的运行时特性做出了某些假设,这些特性无法在编译(或设计)时间保证。同样,按照现代(Java)框架组件的示例,如果您在容器的受控(运行时)环境之外实例化此类,则很容易出现该类无法正常工作的情况,因为其所需的依赖项尚未初始化(也称为注入)。但同样明显的是,组件系统(仅作为一个例子)显然从动态绑定机制中受益。依赖注入是实现这一点的框架级方法。动态作用域是实现相同目的的语言级方法。

5
请注意,将词法作用域的概念(我们认为这是一种编程语言的好东西,而不是动态作用域)与深度嵌入代码中的函数定义(lambda表达式)相结合(也可以称之为“嵌套函数”),可能会变得非常复杂,无论从语言实现还是程序员的角度来看都是如此。甚至有一个专门的名字来描述这个复杂的东西:闭包。
正如维基百科所述
在具有头等嵌套函数的语言中正确实现静态作用域并不容易,因为它要求每个函数值都携带一个记录其依赖变量值的记录(函数和该环境的对组称为闭包)。
这不仅在具有全局和/或可变变量的语言中(如C或Java)实现起来很复杂(考虑确保在闭包评估时对处于作用域中的可变状态进行正确访问!只有一件事:在未来某个时间评估闭包时,使用的对象不应该已被销毁和垃圾回收),而且对于程序员来说,在复杂情况下考虑闭包将如何工作以及它会产生哪些(副)效应也不是一个简单的概念(出于同样的原因:您需要考虑在定义闭包时与所有可变状态交互的方式,例如:当您在闭包定义中引用外部可变变量时,您真的想要访问变量在闭包定义时的值,即,您想要一个只读副本还是您想在未来闭包评估时完全访问变量的可变状态?)。
在纯函数式语言中,思考嵌套函数定义及其用途要简单得多,因此仅具有词法作用域对它们来说根本不是问题。但如果你的语言不是函数式的,这并不是那么容易的事情。(我认为这是 Java 如何添加闭包进行长时间争论的原因之一:它们似乎不够简单,以至于程序员无法理解,尽管它们只是建立在词法作用域的美好概念之上。)
在非纯函数式语言中思考嵌套函数更简单的方法是使用动态作用域(尽管动态作用域并不好:你会失去关于程序正确行为的编译时检查和保证)。
因此,我认为在一种语言中具有动态作用域的优势也可以是可能性,即如果愿意并敢于这样做,可以以简单的方式编程某些东西,尽管存在动态作用域的所有危险。
注: 关于 Java 中闭包的漫长历史(以及程序员不喜欢这个概念)-- http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg04030.html:

Date: Thu, 14 Aug 2003 08:05:44 -0700

From: Michael Vanier

Subject: Re: bindings and assignments (was: Re: continuations)

Date: Thu, 14 Aug 2003 10:45:34 -0400

From: "David B. Tucker"

I imagine, though I don't have statistical evidence, that the requirement of declaring local variables to be final in order to reference them within anonymous inner classes (closures) is almost entirely unknown and unused in practice.

Out of curiosity, does anyone know why Java only allows final variables to be referenced from within anonymous classes?

Dave

   <cynic>Otherwise you'd have the equivalent of true closures,
and if you had that    java would be a
*really* powerful and useful language, so they obviously    couldn't do that.
</cynic>

Actually, the prototype implementation did allow non-final variables to be referenced from within inner classes. There was an outcry from users, complaining that they did not want this! The reason was interesting: in order to support such variables, it was necessary to heap-allocate them, and (at that time, at least) the average Java programmer was still pretty skittish about heap allocation and garbage collection and all that. They disapproved of the language performing heap allocation "under the table" when there was no occurrence of the "new" keyword in sight.

所以,在早期,Java采用了一种“第三种”方法(与我上面提到的两种方法相对),即不是“只读副本”,也不是在闭包定义时实时访问封闭的(可变)状态,而是状态的可变副本(至少,我是这样理解引用的段落的;或者不是的话,他是在谈论堆分配仅仅是引用吗?..那么这就是第二个选项。第三个选项对我来说确实看起来不合理)。我不确定现在他们如何在Java中实现闭包,我没有关注Java的最新内容。


3

动态作用域破坏了引用透明度,这意味着您不能再对程序进行推理。DS基本上是强化版的全局变量。


2
我认为Common LISP中的动态作用域类似于C语言中的全局变量。在函数式函数中使用它们会存在问题。

2
这篇由自由软件基金会创始人、GNU/Linux、Emacs和FSF的理查德·斯托曼所写的经典文章解释了动态作用域对于Emacs编辑器和Emacs Lisp语言的重要性。总之,它对于定制非常有用。

http://www.gnu.org/software/emacs/emacs-paper.html#SEC17

另请参见Emacs维基上的this page,了解有关在Emacs Lisp中使用动态作用域的更多信息:

1

动态作用域变量是一种强大但有时也不直观和危险的工具。

想象一下,您想要具有特定于线程的全局变量,即每个线程都有自己的一组全局变量。这可以很容易地通过动态作用域来实现。只需在线程初始化时更改对这些变量的引用即可。

或者考虑异常:它们在大多数语言中都是动态作用域的。如果您必须从头开始构建异常系统,则可以轻松使用动态作用域变量来完成。


0

Emacs绑定方式对我而言的一个例子 - 顺便提一下,不确定lexical或dynamic是正确术语。

在let内被绑定的变量向下可见,不需要显式的参数传递,这节省了很多按键。

(defun foo1 ()
  (message "%s" a))

(defun foo2 ()
  (let ((a 2))
  (message "%s" a)))

(defun foo3 ()
  (let ((a 1))
    (foo1)
    (foo2)))

==>
1
2

foo2内部的绑定很有意思,因为这里可能安装了默认值的用法

(let ((a (if (eq something a) assign otherwise...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接