方法链包装器 |
|
(" test "):trim():repeatchars(2):upper() --> "TTEESSTT" (function(x,y) return x*y end):curry(2) --> (function(y) return 2*y end)
我们可以使用调试库(debug.setmetatable
)来实现 [5]。缺点是每个内置类型只有一个公共元表。修改此元表会导致全局副作用,这可能是程序中独立维护的模块之间冲突的潜在来源。调试库中的函数通常在常规代码中被不鼓励使用,原因很充分。许多人避免注入这些全局元表,而另一些人则发现它太方便了,无法避免 [3][6][扩展提案]。有些人甚至问为什么内置类型的对象没有自己的元表 [7]。
... debug.setmetatable("", string_mt) debug.setmetatable(function()end, function_mt)
我们可以改用独立函数
(repeatchars(trim("test"), 2)):upper() curry(function(x,y) return x*y end, 2)
这是最简单的解决方案。简单的解决方案通常是好的解决方案。然而,某些操作是方法调用,而另一些操作是独立的全局函数,以及由此产生的重新排序,可能会导致一定程度的不协调。
避免触碰全局元表的一种解决方案是将对象包装在我们自己的类中,在包装器中以方法调用链的方式执行操作,然后解开对象。
示例如下
S" test ":trim():repeatchars(2):upper()() --> TTEESSTT S" TEST ":trim():lower():find('e')() --> 2 2
S
函数将给定对象包装到包装器对象中。对包装器对象的一系列方法调用会就地对包装对象进行操作。最后,包装器对象使用函数调用 ()
解开。
对于返回单个值的函数,另一种解包方法是使用一元减号
-S" test ":trim():repeatchars(2):upper() --> TTEESSTT
要根据字符串函数表 stringx
定义 S
,我们可以使用以下代码
local stringx = {} for k,v in pairs(string) do stringx[k] = v end function stringx.trim(self) return self:match('^%s*(%S*)%s*$') end function stringx.repeatchars(self, n) local ts = {} for i=1,#self do local c = self:sub(i,i) for i=1,n do ts[#ts+1] = c end end return table.concat(ts) end local S = buildchainwrapbuilder(stringx)
buildchainwrapbuilder
函数是通用的,并实现了我们的设计模式
-- (c) 2009 David Manura. Licensed under the same terms as Lua (MIT license). -- version 20090430 local select = select local setmetatable = setmetatable local unpack = unpack local rawget = rawget -- https://lua-users.lua.ac.cn/wiki/CodeGeneration local function memoize(func) return setmetatable({}, { __index = function(self, k) local v = func(k); self[k] = v; return v end, __call = function(self, k) return self[k] end }) end -- unique IDs (avoid name clashes with wrapped object) local N = {} local VALS = memoize(function() return {} end) local VAL = VALS[1] local PREV = {} local function mypack(ow, ...) local n = select('#', ...) for i=1,n do ow[VALS[i]] = select(i, ...) end for i=n+1,ow[N] do ow[VALS[i]] = nil end ow[N] = n end local function myunpack(ow, i) i = i or 1 if i <= ow[N] then return rawget(ow, VALS[i]), myunpack(ow, i+1) end end local function buildchainwrapbuilder(t) local mt = {} function mt:__index(k) local val = rawget(self, VAL) self[PREV] = val -- store in case of method call mypack(self, t[k]) return self end function mt:__call(...) if (...) == self then -- method call local val = rawget(self, VAL) local prev = rawget(self, PREV) self[PREV] = nil mypack(self, val(prev, select(2,...))) return self else return myunpack(self, 1, self[N]) end end function mt:__unm() return rawget(self, VAL) end local function build(o) return setmetatable({[VAL]=o,[N]=1}, mt) end return build end local function chainwrap(o, t) return buildchainwrapbuilder(t)(o) end
测试套件
-- simple examples assert(-S"AA":lower() == "aa") assert(-S"AB":lower():reverse() == "ba") assert(-S" test ":trim():repeatchars(2):upper() == "TTEESSTT") assert(S" test ":trim():repeatchars(2):upper()() == "TTEESSTT") -- basics assert(S""() == "") assert(S"a"() == "a") assert(-S"a" == "a") assert(S(nil)() == nil) assert(S"a":byte()() == 97) local a,b,c = S"TEST":lower():find('e')() assert(a==2 and b==2 and c==nil) assert(-S"TEST":lower():find('e') == 2) -- potentially tricky cases assert(S"".__index() == nil) assert(S"".__call() == nil) assert(S""[1]() == nil) stringx[1] = 'c' assert(S"a"[1]() == 'c') assert(S"a"[1]:upper()() == 'C') stringx[1] = 'd' assert(S"a"[1]() == 'd') -- uncached assert(S"a".lower() == string.lower) -- improve error messages? --assert(S(nil):z() == nil) print 'DONE'
上述实现具有以下特性和假设
__call
和 __index
运算符之外,运算符不遵循方法链样式,这两个运算符也构成了方法调用的两个部分。在 5.1 表中无法定义像 __len
这样的运算符。真正的 Lua 虚拟化 是不可能的。
__call
运算符 ()
来解包,这是唯一允许多个返回值的运算符。代码还支持一元减号作为替代,它有只返回单个值的限制(通常情况),但可能具有更好的语法特性(S 和 -
结合在一起)。
我们可以用其他方法来表达链式调用
S{" test ", "trim", {"repeatchars",2}, "upper"} S(" test ", "trim | repeatchars(2) | upper")
但这看起来不太常规。(注意:最后一行中的第二个参数是无点式 [4]。)
我们可以用以下方式来表达调用链
chain(stringx):trim():repeatchars(5):upper()(' test ')
其中被操作的对象放在最后。这降低了忘记解包的可能性,并且允许分离和重用
f = chain(stringx):trim():repeatchars(5):upper() print ( f(' test ') ) print ( f(' again ') )
有各种方法可以实现这一点(函数式、代码生成 和 VM)。这里我们采用最后一种方法。
-- method call chaining, take #2 -- (c) 2009 David Manura. Licensed under the same terms as Lua (MIT license). -- version 20090501 -- unique IDs to avoid name conflict local OPS = {} local INDEX = {} local METHOD = {} -- table insert, allowing trailing nils local function myinsert(t, v) local n = t.n + 1; t.n = n t[n] = v end local function eval(ops, x) --print('DEBUG:', unpack(ops,1,ops.n)) local t = ops.t local self = x local prev local n = ops.n local i=1; while i <= n do if ops[i] == INDEX then local k = ops[i+1] prev = x -- save in case of method call x = t[k] i = i + 2 elseif ops[i] == METHOD then local narg = ops[i+1] x = x(prev, unpack(ops, i+2, i+1+narg)) i = i + 2 + narg else assert(false) end end return x end local mt = {} function mt:__index(k) local ops = self[OPS] myinsert(ops, INDEX) myinsert(ops, k) return self end function mt:__call(x, ...) local ops = self[OPS] if x == self then -- method call myinsert(ops, METHOD) local n = select('#', ...) myinsert(ops, n) for i=1,n do myinsert(ops, (select(i, ...))) end return self else return eval(ops, x) end end local function chain(t) return setmetatable({[OPS]={n=0,t=t}}, mt) end
基本测试代码
local stringx = {} for k,v in pairs(string) do stringx[k] = v end function stringx.trim(self) return self:match('^%s*(%S*)%s*$') end function stringx.repeatchars(self, n) local ts = {} for i=1,#self do local c = self:sub(i,i) for i=1,n do ts[#ts+1] = c end end return table.concat(ts) end local C = chain assert(C(stringx):trim():repeatchars(2):upper()(" test ") == 'TTEESSTT') local f = C(stringx):trim():repeatchars(2):upper() assert(f" test " == 'TTEESSTT') assert(f" again " == 'AAGGAAIINN') print 'DONE'
另一种想法是修改字符串元表,以便字符串方法的扩展只在词法范围内可见。以下代码并不完美(例如嵌套函数),但这是一个开始。示例
-- test example libraries local stringx = {} function stringx.trim(self) return self:match('^%s*(%S*)%s*$') end local stringxx = {} function stringxx.trim(self) return self:match('^%s?(.-)%s?$') end -- test example function test2(s) assert(s.trim == nil) scoped_string_methods(stringxx) assert(s:trim() == ' 123 ') end function test(s) scoped_string_methods(stringx) assert(s:trim() == '123') test2(s) assert(s:trim() == '123') end local s = ' 123 ' assert(s.trim == nil) test(s) assert(s.trim == nil) print 'DONE'
函数 scoped_string_methods
将给定的函数表分配给当前执行函数的范围。该范围内所有字符串索引都将通过该给定的表进行。
以上代码使用以下框架代码
-- framework local mt = debug.getmetatable('') local scope = {} function mt.__index(s, k) local f = debug.getinfo(2, 'f').func return scope[f] and scope[f][k] or string[k] end local function scoped_string_methods(t) local f = debug.getinfo(2, 'f').func scope[f] = t end
我们可以通过 MetaLua 以更健壮的方式实现类似于上述的方法。以下是一个示例。
-{extension "lexicalindex"} -- test example libraries local stringx = {} function stringx.trim(self) return self:match('^%s*(%S*)%s*$') end local function f(o,k) if type(o) == 'string' then return stringx[k] or string[k] end return o[k] end local function test(s) assert(s.trim == nil) lexicalindex f assert(s.trim ~= nil) assert(s:trim():upper() == 'TEST') end local s = ' test ' assert(s.trim == nil) test(s) assert(s.trim == nil) print 'DONE'
语法扩展引入了一个新的关键字lexicalindex
,它指定了一个函数,该函数将在当前作用域内对值进行索引时被调用。
以下是相应的纯 Lua 代码:
--- $ ./build/bin/metalua -S vs.lua --- Source From "@vs.lua": --- local function __li_invoke (__li_index, o, name, ...) return __li_index (o, name) (o, ...) end local stringx = { } function stringx:trim () return self:match "^%s*(%S*)%s*$" end local function f (o, k) if type (o) == "string" then return stringx[k] or string[k] end return o[k] end local function test (s) assert (s.trim == nil) local __li_index = f assert (__li_index (s, "trim") ~= nil) assert (__li_invoke (__li_index, __li_invoke (__li_index, s, "trim"), "upper" ) == "TEST") end local s = " test " assert (s.trim == nil) test (s) assert (s.trim == nil) print "DONE"
lexicalindex
Metalua 扩展的实现如下:
-- lexical index in scope iff depth > 0 local depth = 0 -- transform indexing expressions mlp.expr.transformers:add(function(ast) if depth > 0 then if ast.tag == 'Index' then return +{__li_index(-{ast[1]}, -{ast[2]})} elseif ast.tag == 'Invoke' then return `Call{`Id'__li_invoke', `Id'__li_index', unpack(ast)} end end end) -- monitor scoping depth mlp.block.transformers:add(function(ast) for _,ast2 in ipairs(ast) do if ast2.is_lexicalindex then depth = depth - 1; break end end end) -- handle new "lexicalindex" statement mlp.lexer:add'lexicalindex' mlp.stat:add{'lexicalindex', mlp.expr, builder=function(x) local e = unpack(x) local ast_out = +{stat: local __li_index = -{e}} ast_out.is_lexicalindex = true depth = depth + 1 return ast_out end} -- utility function -- (note: o must be indexed exactly once to preserve behavior return +{block: local function __li_invoke(__li_index, o, name, ...) return __li_index(o, name)(o, ...) end }