函式鍊包覆器 |
|
(" test "):trim():repeatchars(2):upper() --> "TTEESSTT" (function(x,y) return x*y end):curry(2) --> (function(y) return 2*y end)
我們可以使用偵錯函式庫(debug.setmetatable
) [5] 來做到這件事。缺點是每個內建類型只有一個通用的 metatable。修改這個 metatable 會造成全域性的副作用,這可能是程式中獨立維護的模組之間發生衝突的潛在問題。出於這個原因,偵錯函式庫的函式通常不建議在常規程式碼中使用。許多人避免將程式注入至這些全域性的 metatable,但也有人認為這種方式太方便而無法避免 [3][6][ExtensionProposal]。甚至有些人會詢問為什麼內建類型物件沒有自己的 metatable [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)
這是最簡單的解法。簡單的解法通常是好的。但是,有些操作以函式呼叫的方式進行,而有些操作以獨立的全域函式的形式進行,加上重新排序的結果,可能會導致一定程度的不協調。
避免影響全域 metatable 的其中一種解法是將物件包覆在我們自己的類別中,在函式呼叫鍊中對包覆器執行操作,然後解開包覆的物件。
範例如下所示
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.dev.org.tw/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
運算子,它們也構成了函式呼叫的兩大部分。像是 __len
的運算子無法在 5.1 表格中定義。真正的 LuaVirtualization 做不到這件事。__call
運算子 ()
進行 unpacking,它是允許多個回傳值的唯一運算子。程式碼也支援單元減號作為替代方案,它有只回傳單一值的限制(通常狀況),但或許有比較好的語法品質(S 和 -
一起)。我們可以有其他方法來表示鏈結
S{" test ", "trim", {"repeatchars",2}, "upper"} S(" test ", "trim | repeatchars(2) | upper")
不過這樣看來比較不傳統。(注意:最後一行的第二個引數是 point-free [4]。)
我們也可以像這樣表達呼叫鏈
chain(stringx):trim():repeatchars(5):upper()(' test ')
其中操作的物件會放在最尾端。這樣可以降低忘記 unpack 的機會,並允許分離和重複使用
f = chain(stringx):trim():repeatchars(5):upper() print ( f(' test ') ) print ( f(' again ') )
有很多方法可以實作這個(函式、CodeGeneration 和 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 }