表格作用域 |
|
本文描述了各种解决方案,允许表格结构包含一个作用域,以便在表格内部使用的变量具有特殊含义。换句话说,我们想要类似这样的东西
local obj = struct "foo" { int "bar"; string "baz"; } assert(obj == "foo[int[bar],string[baz]]") assert(string.lower("ABC") == "abc") assert(int == nil)
Lua 非常适合数据定义(如《Programming in 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"; }
在这种情况下,int
和 string
现在是字符串而不是全局变量。但是,这里参数的顺序和数量丢失了(它们在结构体中很重要)。另一种方法是
struct "name" . int "bar" . string "baz" . endstruct
从语义上讲,这更好(除了必须记住 endstruct
),但点语法有点不寻常。从语义上讲,我们可能想要类似于 S 表达式的
struct { "name", {"int", "bar"}, {"string", "baz"} }
但语法上却有所欠缺。
有一个“表格作用域”补丁允许这种类型的操作
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 编译),这些假设可能无法满足。但是,在某个函数中不满足这些假设的效果通常是,表格作用域 simply 不应用于该函数,尽管可能存在非常不寻常的情况,这些情况会影响安全性。
可能有一些方法可以减少使用此补丁对全局访问的性能影响。欢迎提出建议。例如,可以有选择地启用或禁用特定函数的表格作用域查找。如果性能是一个问题,你应该始终使用局部变量。
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
形式可能没问题。只需注意使用它的契约:如果它引发异常,则丢弃调用它的函数。对于配置语言来说,这可能是一个很好的方法。
但是,请注意,以上方法对于某些检测未定义变量的方法并不适用。rectangle
和point
可能会被识别为未定义变量,尤其是在对未定义全局变量进行静态检查时(这些是顶级脚本中未定义的全局/环境变量)。
如果我们不需要特别访问上值,我们可以将上述数据函数字符串化(参见简短匿名函数详细信息中的“字符串化匿名函数”模式)。
local o = pwith(shapes)[[ rectangle { point(x,y) } ]]{x = 3, y = 4}
我们失去了对调用者中词法变量的直接访问,但pwith
可以在数据字符串前面添加局部变量,以便rectangle
和point
(以及x
和y
)成为词法变量。pwith
可以像这样实现它,前提是不超过最大词法限制。
local code = [[ local rectangle, point, x, y = ... ]] .. datasttring local f = loadstring(code)(namespace.rectangle, namespace.point, x, y)
以下是 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.0-work3 删除了setfenv
(尽管在调试库中保留了对它的部分支持)。这使上面许多“环境技巧”失效,尽管它们本来也不太好。
在 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 的参数)。