表作用域

lua-users home
wiki

摘要

这里描述了各种解决方案,允许表结构来封装一个作用域,使得在表中使用的变量具有特殊含义。换句话说,我们想要这样的东西:

local obj = struct "foo" {
              int "bar";
              string "baz";
            }
assert(obj == "foo[int[bar],string[baz]]")
assert(string.lower("ABC") == "abc")
assert(int == nil)

问题

Lua 非常适合数据定义(如《Lua 编程》中所讨论的那样)。

local o = rectangle { point(0,0), point(3,4) }

-- another example:
html{ h1"This is a header", p"This is a paragraph",
      p{"This is a ", em"styled", " paragraph"} }

-- another example:
struct "name" {
  int    "bar";
  string "baz";
}

语法很好。语义就没那么好了:string 是一个全局变量,作用域在 struct 外部,即使它只在 struct 内部有意义。这会产生问题。例如,上面的例子需要重新定义标准的 string 表。

要求加前缀可以解决问题,但可能很麻烦且难看,这与数据定义旨在描述的问题领域格格不入。

local struct = require "struct"
...
local o = struct "name" {
            struct.int  "bar";
            struct.string "baz";
          }

我们可以用局部变量来限制作用域,但定义它们也可能变得很麻烦,尤其是在你的数据定义语言包含数百个标签的情况下。

local struct = require "struct"
...
local o; do
  local int = struct.int
  local string = struct.string
  o = struct "name" {
        int  "bar";
        string "baz";
      }
end

事实上,我们可能希望某个词根据嵌套上下文具有不同的含义。

action { point { at = location { point(3,4) } }

替代方案可能是:

struct "name" {
  int = "bar";
  string = "baz";
}

在这种情况下,intstring 现在是字符串而不是全局变量。但是,这里丢失了参数的顺序和数量(这在结构体中很重要)。另一种替代方案是:

struct "name" .
  int "bar" .
  string "baz" .
endstruct

语义上,这更好(除了必须不要忘记 endstruct),但点语法有点不寻常。语义上我们可能想要类似 S 表达式的东西:

struct {
  "name",
  {"int", "bar"},
  {"string", "baz"}
}

但语法上有所欠缺。

解决方案:Lua 的表作用域补丁

有一个“表作用域”补丁允许这种类型的操作。

local function struct(name)
  local scope = {
    int = function(name) return "int[" .. name .. "]" end;
    string = function(name) return "string[" .. name .. "]" end
  }
  return setmetatable({}, {
    __scope = scope;
    __call = function(_, t)
               return name .. "[" .. table.concat(t, ",") .. "]"
             end
  })
end

local obj = struct "foo" {
              int "bar";
              string "baz";
            }
assert(obj == "foo[int[bar],string[baz]]")
assert(string.lower("ABC") == "abc")
assert(int == nil)
print "DONE"

下载补丁:[tablescope.patch] (适用于 Lua 5.1.3)

该补丁对 Lua 语法或 Lua 字节码没有任何改变。唯一的改变是对一个名为 __scope 的新元方法的支持。如果一个表结构被用作函数调用的最后一个参数,并且被调用的对象包含一个 __scope 元方法(该元方法是一个表),那么在表结构中引用的全局变量将首先在 __scope 中查找。只有在 __scope 中找不到时,才会像往常一样在环境表中查找变量。

该补丁在推断表如何嵌套全局变量访问时,对字节码的顺序做出了一些假设。如果字节码不是由 luac 编译的(例如,由 MetaLua 编译),则可能无法满足这些假设。然而,在某些函数中无法满足这些假设的后果通常是表作用域根本不应用于该函数,尽管可能存在一些非常罕见的具有安全隐患的特殊情况。

可能有一些方法可以减少此补丁对全局访问的性能影响。欢迎提出建议。例如,可以在特定函数上选择性地启用或禁用表作用域查找。如果性能是关注点,你应该使用局部变量。

MetaLua 生成的字节码与 luac 完全相同,除非你使用 Goto 或 Stat。此外,如果你使用 MetaLua,你也会用它来处理表作用域,而不是修补 Lua -- FabienFleutot

解决方案:全局环境技巧

避免修补,我们可以利用 Lua 环境表做一些技巧。下面的模式可能会被使用(原始想法由 RiciLake 提出)。

-- shapes.lua
local M = {}

local Rectangle = {
  __tostring = function(self)
    return string.format("rectangle[%s,%s]",
      tostring(self[1]), tostring(self[2]))
  end
}
local Point = {
  __tostring = function(self)
    return string.format("point[%f,%f]",
      tostring(self[1]), tostring(self[2]))
  end
}

function M.point(x,y)
  return setmetatable({x,y}, Point)
end
function M.rectangle(t)
  local point1 = assert(t[1])
  local point2 = assert(t[2])
  return setmetatable({point1, point2}, Rectangle)
end

return M

-- shapes_test.lua

-- with: namespace, [level], [filter] --> (lambda: ... --> ...)
function with(namespace, level, filter)
  level = level or 1; level = level + 1

  -- Handle __with metamethod if defined.
  local mt = getmetatable(namespace)
  if type(mt) == "table" then
    local custom_with = mt.__with
    if custom_with then
      return custom_with(namespace, level, filter)
    end
  end

  local old_env = getfenv(level)  -- Save

  -- Create local environment.
  local env = {}
  setmetatable(env, {
    __index = function(env, k)
      local v = namespace[k]; if v == nil then v = old_env[k] end
      return v
    end
  })
  setfenv(level, env)

  return function(...)
    setfenv(2, old_env)       -- Restore
    if filter then return filter(...) end
    return ...
  end
end

local shapes = require "shapes"

local o = with(shapes) (
  rectangle { point(0,0), point(3,4) }
)
assert(not rectangle and not point) -- note: not visible here
print(o)
-- outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

关键在于 with 函数,它提供了对给定命名空间的本地访问。其目的与某些其他语言(如 VB)中的“with”子句类似,并且在某种程度上与 C++ 中的 using namespace 或 Java 中的 import static[1] 相关。它也可能类似于 XML 命名空间。

下面的特殊情况可以正确地输出相同的结果:

point = 2
function calc(x) return x * point end
local function calc2(x) return x/2 end
local o = with(shapes) ( rectangle { point(0,0), point(calc2(6),calc(2)) } )
print(o)
-- outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

with 的可选参数在定义包装器以简化表达式时可能很有用。

function shape_context(level)
  return with(shapes, (level or 1)+1, function(x) return x[1] end)
end

local o = shape_context() {
  rectangle { point(0,0), point(3,4) }
}
print(o)
-- outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

通过自动调用 with 来访问全局键,可以进一步简化:

setmetatable(_G, {
  __index = function(t, k)
    if k == "rectangle" then
       return with(shapes, 2, function(...) return shapes.rectangle(...) end)
    end
  end
})

local o = rectangle { point(0,0), point(3,4) }
print(o)
-- outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

一个警告是,这种方法依赖于未记录的 Lua 行为。函数名 with 必须在函数参数解析之前解析,这在 Lua 5.1 的当前版本中是这种行为。

此外,尽管期望 with 提供一种词法作用域,并且它模拟得相当好,但实际实现更加动态。以下操作将导致运行时错误,因为真正的词法作用域(局部变量)会覆盖全局变量:

local point = 123
local o = with(shapes) ( rectangle { point(0,0), point(3,4) } )

此外,我们假设调用者的环境可以暂时被替换而不会产生不良影响。

不调用最后一个函数(意外地)不一定会导致错误,但会留下已更改的环境。

local o = with(shapes)
assert(rectangle) -- opps! rectangle is visible now.

但这确实表明了这种用法是可能的(尽管不一定是个好主意):

local f = with(shapes) -- begin scope
local o1 = rectangle { point(0,0), point(3,4) }
local o1 = rectangle { point(0,0), point(5,6) }
f() -- end scope
assert(not rectangle) -- rectangle no longer visible

另一个问题是,如果评估参数时出现异常,环境将不会恢复。

local last = nil
function test()
  last = rectangle
  local o = with(shapes) ( rectangle { point(3,4) } ) -- raises error
end
assert(not pcall(test))
assert(not last)
assert(not pcall(test))
assert(last)  -- opps! environment not restored

不幸的是,似乎没有一种不显眼的方法可以将参数包装到 pcall 中。我们可以使用一个新的 pwith 函数来实现这一点,该函数接受用笨拙的 function() return ... end 语法包装的数据,以便稍后由 pcall 进行评估。

local o = pwith(shapes)(function() return
  rectangle { point(3,4) } -- raises error
end)

事实上,这种方法可以避免依赖未记录行为、漏掉第二个调用以及修改调用者环境的危险。我们只需要接受这种语法(另请参阅上面的“全局收集器”模式,该模式应用了类似的语法和语义)。

另一种变体是在事后调用 pwith(例如,在配置文件之外)。

rect = function() return rectangle { point(3,4) } end
...
pwith(shapes)(rec)

或者,pwith 可以由 _G 上的 __newindex 元方法事件触发。

非 pcall 形式可能没问题。只需注意使用它的合同:如果它引发异常,则丢弃调用它的函数。这对于配置文件来说可能是一个很好的方法。

但是请注意,上述方法对于某些 检测未定义变量 的方法效果不佳。rectanglepoint 可能被识别为未定义变量,尤其是在静态检查未定义全局变量(这些是未在顶级脚本中定义的全局/环境变量)时。

如果我们不需要对 upvalues 进行特殊访问,我们可以将上述数据函数字符串化(参见 简短匿名函数 详情中的“字符串化匿名函数”模式)。

local o = pwith(shapes)[[
  rectangle { point(x,y) }
]]{x = 3, y = 4}

我们失去了对调用者中词法作用域的直接访问,但 pwith 可以将局部变量添加到数据字符串的前面,这样 rectanglepoint(以及 xy)就变成了词法变量。pwith 可以通过以下方式实现,只要不达到最大词法变量限制:

local code = [[
  local rectangle, point, x, y = ...
]] .. datasttring
local f = loadstring(code)(namespace.rectangle, namespace.point, x, y)

解决方案:使用 Metalua

这是 Metalua 中的一个实现:

-- with.lua

function with_expr_builder(t)
  local namespace, value = t[1], t[2]
  local tmp = mlp.gensym()
  local code = +{block:
    local namespace = -{namespace}
    local old_env = getfenv(1)
    local env = setmetatable({}, {
      __index = function(t,k)
        local v = namespace[k]; if v == nil then v = old_env[k] end
        return v
      end
    })
    local -{tmp}
    local f = setfenv((|| -{value}), env)
    local function helper(success, ...)
      return {n=select('#',...), success=success, ...}
    end
    let -{tmp} = helper(pcall(f))
    if not -{tmp}.success then error(-{tmp}[1]) end
  }
  -- NOTE: Stat seems to only support returning a single value.
  --       Multiple return values are ignored (even though attempted here)
  return `Stat{code, +{unpack(-{tmp}, 1, -{tmp}.n)}}
end

function with_stat_builder(t)
  local namespace, block = t[1], t[2]
  local tmp = mlp.gensym()
  local code = +{block:
    local namespace = -{namespace}
    local old_env = getfenv(1)
    local env = setmetatable({}, {
      __index = function(t,k)
        local v = namespace[k]; if v == nil then v = old_env[k] end
        return v
      end
    })
    local -{tmp}
    local f = setfenv(function() -{block} end, env)
    local success, msg = pcall(f)
    if not success then error(msg) end
  }
  return code
end

mlp.lexer.register { "with", "|" }

mlp.expr.primary.add {
  "with", "|", mlp.expr, "|", mlp.expr,
  builder=with_expr_builder
}

mlp.stat.add {
  "with", mlp.expr, "do", mlp.block, "end",
  builder=with_stat_builder
}

用法示例

-{ dofile "with.luac" }

local shapes = require "shapes"
rectangle = 123  -- no problem

local o = with |shapes|
          rectangle { point(0,0), point(3,4) }
print(o)
--outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

local o
with shapes do
  o = rectangle { point(0,0), point(3,4) }
end
print(o)
--outputs: rectangle[point[0.000000,0.000000],point[3.000000,4.000000]]

local other = {double = function(x) return 2*x end}

local o = with |other|
          with |shapes|
          rectangle { point(0,0), point(3,double(4)) }
print(o)
--outputs: rectangle[point[0.000000,0.000000],point[3.000000,8.000000]]

local o
local success, msg = pcall(function()
  o = with |shapes| rectangle { point(0,0) }
end)
assert(not success)
assert(rectangle == 123) -- original environment

更新 (2010-06):上述 setfenv 可以避免,在 Lua 5.2.0-work3 中也必须避免(见下文)。

Lua 5.2

Lua-5.2.0-work3 删除了 setfenv(尽管在 debug 库中保留了对它的部分支持)。这使得上面许多“环境技巧”技术失效,尽管它们本来也就不太好。

在 Lua-5.2.0-work3 中,_ENV 允许:

local o = (function(_ENV) return rectangle { point(0,0), point(3,4) } end)(shapes)

这与上面 5.1 解决方案“pwith(shapes)(function() return ..... end)”具有相似的特性,但不需要 pcall。上面的额外函数仅用于在表达式中间引入词法作用域(_ENV),但如果我们在单独的语句中定义 _ENV,我们可以删除该函数。

local o; do local _ENV = shapes
  o = rectangle { point(0,0), point(3,4) }
end

这在语法上可能更清晰,但也不是那么糟糕,除非我们需要频繁地切换环境(例如,在各自的环境中评估 shapes、rectangle 和 point 的参数)。


--DavidManura, 2007/2008

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2010 年 6 月 14 日下午 9:11 GMT (差异)