表格作用域

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 非常适合数据定义(如《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";
}

在这种情况下,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 编译),这些假设可能无法满足。但是,在某个函数中不满足这些假设的效果通常是,表格作用域 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形式可能没问题。只需注意使用它的契约:如果它引发异常,则丢弃调用它的函数。对于配置语言来说,这可能是一个很好的方法。

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

如果我们不需要特别访问上值,我们可以将上述数据函数字符串化(参见简短匿名函数详细信息中的“字符串化匿名函数”模式)。

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(尽管在调试库中保留了对它的部分支持)。这使上面许多“环境技巧”失效,尽管它们本来也不太好。

在 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

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2010 年 6 月 15 日凌晨 3:11 GMT (差异)