Lua 作用域讨论

lua-users home
wiki

此页面曾试图用常见的词汇来描述 Lua 的作用域,但最终演变成了一场关于作用域术语的辩论。若想了解用通俗易懂的语言描述 Lua 作用域,请参见 LuaScoping

版本说明:此页面上的评论参考的是一个相当古老的 Lua 版本(2001 年左右),该版本具有更有限的词法作用域,可能还有不完整的闭包支持。其中许多评论不适用于最近版本的 Lua。

Lua 是静态作用域 [1]

有限的静态作用域

关于嵌套函数对变量的访问存在一个问题。嵌套函数无法访问其外部作用域中除全局变量以外的其他变量。为了提供对变量副本(在匿名函数实例化时)的只读访问,提供了一种特殊的“upvalue”语法,但仅限于定义嵌套函数的直接外部作用域。

关于这种情况是否应称为有限/损坏的静态/词法作用域,存在一些分歧。一些计算机语言书籍将词法作用域和静态作用域定义为相同,这使得问题更加复杂。当访问的变量处于全局作用域时,Lua 的行为确实是词法作用域。

do
    -- assume "a" is a global with value 5
    local foo = function() print(a) end
    a = 10
    foo() -- prints "10"
end

当使用 upvalue 访问下一个外部作用域时,只要变量没有被重新绑定(无论是从变量的作用域还是从嵌套函数),Lua 的行为也像是词法作用域。

do
    local a = 5
    local foo = function() print(%a) end
    foo() -- prints "5"
end

无论如何,Lua 可能不像早期的一些脚本语言(如 Perl)那样糟糕。Perl 最初是动态作用域。它已慢慢演变,通过 my 限定符支持词法作用域。词法作用域可能在 Perl 6 中成为默认设置。

Python 之前也具有类似 Lua 的有限静态作用域。在 2.2 版本中,词法作用域将成为默认设置,并且在 2.1 中已作为一种选项 [2]。Python 的词法作用域实现不允许从嵌套函数内部重新绑定变量,这与 Lua 的 upvalue 类似。

对于 Lua 来说,也许只需要将其有限的子集扩展为真正的词法作用域。它可以延续 upvalue 的概念,并且不允许像 Python 那样重新绑定。或者,它也可以允许重新绑定,从而消除 upvalue 的需求,而 upvalue 常常是 Lua 程序员感到困惑的根源。在 Python 词法作用域的设计文档中,不允许多重绑定的主要原因是 Python 缺乏变量声明。由于 Lua 需要 local 来限定变量,这不会成为一个问题。

Notes

顺便说一句,Lua **总是**采用词法作用域。没有例外。没有动态作用域规则。编译器决定采用哪个名称声明,如果无法处理某种情况(外部函数的局部变量),则会发出错误。所以,问题不在于 Lua 有时是词法作用域还是不是;它始终是。Lua 只是在允许的范围内存在一些限制。(词法/动态作用域定义的是**如何**进行名称解析,而不是允许什么。)

-- E. Toernig

你得去找 John Ramsdell 争论这个问题,我置身事外 :) 。我并不同意 Lua 是静态作用域的说法。困惑源于有些人区分“静态作用域”和“词法作用域”。--JohnBelmonte

我感到困惑。我认为我理解“静态”和“动态”作用域的区别。人们在“静态”和“词法”之间划出的区别是什么?--JamesHearn

我认为困惑之处在于,在通用用法中,对“词法”和“静态”作用域这两个术语存在两种不兼容的定义。函数式编程社区过去使用这些术语来表示包含嵌套作用域要求的内容,然而,“龙书”的定义明确暗示 Lua 既是词法作用域又是静态作用域。-- John D. Ramsdell

摘自 Aho, Sethi 和 Ullman 著《编译原理、技术和工具》(1986 年,Addison-Wesley)第 411 页

语言的作用域规则决定了对非局部名称引用的处理方式。一种常见的规则,称为**词法作用域**或**静态作用域规则**,通过单独检查程序文本来确定适用于某个名称的声明。Pascal、C 和 Ada 等许多语言都使用词法作用域,并附加了下面将讨论的“最紧密嵌套”的规定。另一种规则,称为**动态作用域规则**,通过考虑当前激活来在运行时确定适用于某个名称的声明。Lisp、APL 和 Snobol 等语言使用动态作用域。

将 FOLDOC 条目与“龙书”中的内容进行比较,后者的定义似乎更宽松一些,因为它没有将“最小块”的规定视为必需,而只将其视为某些语言(如 C)的属性。换句话说,只要某个作用域规则不是动态的(即不依赖于当前激活),就可以被认为是静态的。

我对“有限静态作用域”和“真正的词法作用域”之类的术语感到困惑。语言要么是静态作用域,要么就不是;要么是词法作用域,要么就不是。在这些术语前面加上修饰词是毫无意义的。-- John D. Ramsdell

Lua 目前缺乏静态嵌套作用域

在 Lua 语言中,每个变量要么是局部作用域,要么是全局作用域。函数可以定义在函数内部,但是,嵌套函数无法访问其任何外层函数中定义的变量。因此,Lua 变量缺乏静态嵌套作用域。结果,以下 Lua 代码是非法的:

function addn(x)
  function sum(y)
    return x+y
  end
  return sum
end
print((addn(3))(1))

此示例是非法的,因为在 addn 中定义的变量 x 对嵌套函数 sum 不可访问。

嵌套函数可以访问其直接外层函数中定义的变量的副本。对函数内的变量的 upvalue 引用在函数被求值以生成闭包时提取此副本。以百分号(%)开头的变量引用表示 upvalue 引用。在 sum 中,在 x 引用前加上百分号,可以使上述示例成为合法的 Lua 代码。

这种对静态嵌套作用域的糟糕替代方案被采用,是因为它易于实现,使得所有非全局变量都可以分配在栈上,但是,不必放弃静态嵌套作用域就可以拥有分配在栈上的非全局变量。

2.2 版本之前的 Python 语言具有与 Lua 类似的当前规则——每个变量要么是局部作用域,要么是全局作用域。从 2.2 版本开始,Python 具有静态嵌套作用域。在 Python 的实现中,从嵌套函数中引用的非全局变量是不可变的。有了这个额外的假设,栈分配的非全局变量很容易实现。

Python 的实现证明了这种方法的有效性。正如 Luca Cardelli 在 1984 年的论文《编译函数式语言》[3] 中所述,他们通过使用扁平闭包(flat closures)实现了快速实现。相关部分是关于“获取变量”的第 4 部分。

对于我们这些希望 Lua 未来版本能够拥有静态嵌套作用域变量的人来说,我建议我们可以通过寻找可以被核心 Lua 实现者使用的实现技术来提供帮助。我认为我们应该仔细研究 Python 2.2 实现的相关部分以获取灵感,并使其可用。

我不是 Python 程序员,但这些评论让我得以一窥 Python 的作用域规则。如果我有什么不对的地方,请纠正我。据我所知,Python 2.0 在嵌套函数方面与 Lua 存在完全相同的问题。无法访问外部局部变量,并且局部递归函数不可用。用于将变量传递给局部函数的方法是通过名为 name=name 的命名参数实现的。第一个 name 是函数的形参名,第二个 name 表示在函数声明时冻结的外部作用域的值。因此,这与 Lua 中的 upvalue 行为相同。这只是语法不同:在 Python 中,您会在参数列表中放一个 foo=foo 来访问外部 foo,在 Lua 中,您会在函数内写 %foo 来访问外部变量。语义相同。特别是,您无法更改外部变量的值,并且对外部局部变量的后续更改将不会对已声明的函数可见。Lua 有一个奇怪的限制(可以轻松移除且没有向后兼容性问题),即您只能访问直接外层作用域。我猜 Python 也有同样的问题——您必须将值从一个函数传递到另一个函数。

在 Python 2.2 中,他们更改了作用域规则,以便编译器自动生成等效于 name=name 参数的内容。您仍然无法更改外部名称的值。但不知何故,他们现在可以调用局部函数进行递归了。(相互调用的两个局部函数呢?可以吗?)也许这与一个奇怪的现象有关,即在局部函数之后声明的局部变量可以被该函数访问。但它仍然不是 Pascal 或 Scheme 等语言中的那种词法作用域。

我发布到 lua-l 的补丁(针对 Lua 3.2),标题为“可写 upvalue”(writeable upvalues),实现了 Python 2.2 的外部局部变量语义。唯一缺失的魔法是递归函数调用。但这似乎并不是大多数人(包括我——主要是因为递归函数调用问题)想要看到的东西。

-- ET

我相信用一种优秀的、非常高级的语言编写的程序,应该能让偶尔熟悉该语言的人轻松阅读。除了 upvalue 之外,我认为 Lua 在这方面表现出色,因为它采用了类似 Pascal 的语法来实现其含义正如人们所期望的控制结构。使用 upvalue 的程序不太可能被普通用户理解。人们需要手册才能理解它们何时获得值。大多数人凭直觉就能理解静态嵌套作用域。当变量从嵌套函数内部被引用时,将变量设为不可变会给 Lua 程序的作者带来负担,但不会给程序的读者带来负担。我认为 Lua 的设计应首先满足读者的需求。

以下是静态嵌套作用域的定义

在一个静态嵌套作用域的语言中,一个标识符的作用域在编译时被固定为包含该标识符声明的最小块(begin/end 或函数/过程体)。这意味着在一个块中声明的标识符只能在该块内部及其内部声明的过程(procedures)中访问。

-- John D. Ramsdell

参考文献

摘自《如何设计程序》(一本基于 Scheme 的教学文本)

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2008 年 3 月 29 日下午 6:49 GMT (差异)