Vararg 二等公民 |
|
...
」[1] 在 Lua 5.1 中不是[一級]物件,這導致表達式中有些限制。以下提供這些問題及解決方法。
Lua 5.1 變數參數 (...
) 的處理有些限制。例如,不允許像這樣的東西
function tuple(...) return function() return ... end end --Gives error "cannot use '...' outside a vararg function near '...'"
(有關這方面的說明,請見 LuaList:2007-03/msg00249.html。)
您可能會想要使用此類函式,以暫時儲存函式呼叫的傳回值、執行其他工作,然後再次擷取這些儲存的傳回值。下方的函式會使用此假設的 tuple
函式,在指定的函式周圍加入追蹤敘述
--Wraps a function with trace statements. function trace(f) return function(...) print("begin", f) local result = tuple(f(...)) print("end", f) return result() end end test = trace(function(x,y,z) print("calc", x,y,z); return x+y, z end) print("returns:", test(2,3,nil)) -- Desired Output: -- begin function: 0x687350 -- calc 2 3 nil -- end function: 0x687350 -- returns: 5 nil
不過,Lua 中有方法可以達成這個目的。
{...}
和 unpack
您可以使用表格建構函式 {...}
和 unpack
來實作 trace
--Wraps a function with trace statements. function trace(f) return function(...) print("begin", f) local result = {f(...)} print("end", f) return unpack(result) end end test = trace(function(x,y,z) print("calc", x,y,z); return x+y, z end) print("returns:", test(2,3,nil)) -- Output: -- begin function: 0x6869d0 -- calc 2 3 nil -- end function: 0x6869d0 -- returns: 5
很遺憾,它不會傳回 nil
,這是因為 nil
無法明確儲存在表格中,而且特別 {...}
無法保留尾端 nil
的資訊(這部分會在 StoringNilsInTables 中進一步說明)。
{...}
和 unpack
搭配 n
針對上一個解決方法的改良版本,可以適當地處理 nil
function pack2(...) return {n=select('#', ...), ...} end function unpack2(t) return unpack(t, 1, t.n) end --Wraps a function with trace statements. function trace(f) return function(...) print("begin", f) local result = pack2(f(...)) print("end", f) return unpack2(result); end end test = trace(function(x,y,z) print("calc", x,y,z); return x+y, z end) print("returns:", test(2,3,nil)) -- Output: -- begin function: 0x6869d0 -- calc 2 3 nil -- end function: 0x6869d0 -- returns: 5 nil
Shirik 提出的變體是
local function tuple(...) local n = select('#', ...) local t = {...} return function() return unpack(t, 1, n) end end
nil
佔位符以下方法會將 nil
換成可以在表格中儲存的佔位符。它在這裡可能不太適當,但這種方法在其他情況下或許可以用。
local NIL = {} -- placeholder value for nil, storable in table. function pack2(...) local n = select('#', ...) local t = {} for i = 1,n do local v = select(i, ...) t[i] = (v == nil) and NIL or v end return t end function unpack2(t) --caution: modifies t if #t == 0 then return else local v = table.remove(t, 1) if v == NIL then v = nil end return v, unpack2(t) end end --Wraps a function with trace statements. function trace(f) return function(...) print("begin", f) local result = pack2(f(...)) print("end", f) return unpack2(result) end end test = trace(function(x,y,z) print("calc", x,y,z); return x+y, z end) print("returns:", test(2,3,nil)) -- Output: -- begin function: 0x687350 -- calc 2 3 nil -- end function: 0x687350 -- returns: 5 nil
以下是 pack2 和 unpack2 的更適當實作
local NIL = {} -- placeholder value for nil, storable in table. function pack2(...) local n = select('#', ...) local t = {...} for i = 1,n do if t[i] == nil then t[i] = NIL end end return t end function unpack2(t, k, n) k = k or 1 n = n or #t if k > n then return end local v = t[k] if v == NIL then v = nil end return v, unpack2(t, k + 1, n) end
另請參閱 StoringNilsInTables。
如果我們使用延續傳遞樣式 (CPS)([維基百科]),如下所示,則可以避免表格。我們可以預期這樣做會更有效率。
function trace(f) local helper = function(...) print("end", f) return ... end return function(...) print("begin", f) return helper(f(...)) end end test = trace(function(x,y,z) print("calc", x,y,z); return x+y, z end) print("returns:", test(2,3,nil)) -- Output: -- begin function: 0x686b10 -- calc 2 3 nil -- end function: 0x686b10 -- returns: 5 nil
CPS 方法也用於 RiciLake 的字串分割函式中 (LuaList:2006-12/msg00414.html)。
另一種方法是進行程式碼產生,其會針對每個元組大小編譯獨立的建構函式。建構這些建構函式的初期成本有些高,但這些建構函式本身可以獲得最佳實作。之前所使用的 tuple
函式可以如此實作
local function build_constructor(n) local t = {}; for i = 1,n do t[i] = "a" .. i end local arglist = table.concat(t, ',') local src = "return function(" .. arglist .. ") return function() return " .. arglist .. " end end" return assert(loadstring(src))() end function tuple(...) local construct = build_constructor(select('#', ...)) return construct(...) end
為避免每次儲存時的程式碼產生開銷,我們可以對 make_storeimpl
函式進行記憶化處理(欲深入了解背景,請參閱 [維基百科:記憶化] 與 FuncTables)。
function Memoize(fn) return setmetatable({}, { __index = function(t, k) local val = fn(k); t[k] = val; return val end, __call = function(t, k) return t[k] end }) end build_constructor = Memoize(build_constructor)
透過程式碼產生實作的更完整元組範例可在 FunctionalTuples 中找到。
程式碼建置/記憶化技術和上述 Memoize
函式基於 RiciLake 的一些先前相關範例,例如 [主題:Curry 挑戰]。
此外請注意,如果封裝好的函式會引發例外狀況,您會想要另外進行 pcall
(LuaList:2007-02/msg00165.html)。
下列方法純粹是函數式的(無表格),且可避免程式碼產生。這不一定是效率最高的做法,因為此做法會為每個元組元素建立一個函式。
function helper(n, first, ...) if n == 1 then return function() return first end else local rest = helper(n-1, ...) return function() return first, rest() end end end function tuple(...) local n = select('#', ...) return (n == 0) and function() end or helper(n, ...) end -- TEST local function join(...) local t = {n=select('#', ...), ...} for i=1,t.n do t[i] = tostring(t[i]) end return table.concat(t, ",") end local t = tuple() assert(join(t()) == "") t = tuple(2,3,nil,4,nil) assert(join(t()) == "2,3,nil,4,nil") print "done"
另一個點子是使用協同程式
do local function helper(...) coroutine.yield() return ... end function pack2(...) local o = coroutine.create(helper) coroutine.resume(o, ...) return o end function unpack2(o) return select(2, coroutine.resume(o)) end end
在 LuaList:2007-02/msg00142.html 中貼出了一則類似建議。不過這可能會導致效率不佳(RiciLake 指出:最小的協同程式會占用略超過 1k,外加 malloc 開銷;在 FreeBSD 中總計接近 2k,而最大的部分是堆疊,其預設使用 45 個槽,每個槽 12 或 16 位元)。
不需要每次呼叫就建立新的協同程式。下列方法相當有效率,且遞迴使用尾呼叫
local yield = coroutine.yield local resume = coroutine.resume local function helper(...) yield(); return helper(yield(...)) end local function make_stack() return coroutine.create(helper) end -- Example local stack = make_stack() local function trace(f) return function(...) print("begin", f) resume(stack, f(...)) print("end", f) return select(2, resume(stack)) end end
元組可以在 C 中實作為一個包含元組元素(作為上值)的閉包。程式設計 in Lua 第 2 版 第 27.3 節中展示了這一點 [2]。
在下列效能測試中比較了上述解決方案的速度。
-- Avoid global table accesses in benchmark. local time = os.time local unpack = unpack local select = select -- Benchmarks function f using chunks of nbase iterations for duration -- seconds in ntrials trials. local function bench(duration, nbase, ntrials, func, ...) assert(nbase % 10 == 0) local nloops = nbase/10 local ts = {} for k=1,ntrials do local t1, t2 = time() local nloops2 = 0 repeat for j=1,nloops do func(...) func(...) func(...) func(...) func(...) func(...) func(...) func(...) func(...) func(...) end t2 = time() nloops2 = nloops2 + 1 until t2 - t1 >= duration local t = (t2-t1) / (nbase * nloops2) ts[k] = t end return unpack(ts) end local function print_bench(name, duration, nbase, ntrials, func, ...) local fmt = (" %0.1e"):rep(ntrials) print(string.format("%25s:" .. fmt, name, bench(duration, nbase, ntrials, func, ...) )) end -- Test all methods. local function test_suite(duration, nbase, ntrials) print("name" .. (", t"):rep(ntrials) .. " (times in sec)") do -- This is a base-line. local function trace(f) return function(...) return f(...) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("(control)", duration, nbase, ntrials, f, 1,2,3,4,5) end do local function trace(f) local function helper(...) return ... end return function(...) return helper(f(...)) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("CPS", duration, nbase, ntrials, f, 1,2,3,4,5) end do local yield = coroutine.yield local resume = coroutine.resume local function helper(...) yield(); return helper(yield(...)) end local function make_stack() return coroutine.create(helper) end local stack = make_stack() local function trace(f) return function(...) resume(stack, f(...)) return select(2, resume(stack)) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("Coroutine", duration, nbase, ntrials, f, 1,2,3,4,5) end do local function trace(f) return function(...) local t = {f(...)} return unpack(t) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("{...} and unpack", duration, nbase, ntrials, f, 1,2,3,4,5) end do local function trace(f) return function(...) local n = select('#', ...) local t = {f(...)} return unpack(t, 1, n) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("{...} and unpack with n", duration, nbase, ntrials, f, 1,2,3,4,5) end do local NIL = {} local function pack2(...) local n = select('#', ...) local t = {...} for i=1,n do local v = t[i] if t[i] == nil then t[i] = NIL end end return t end local function unpack2(t) local n = #t for i=1,n do local v = t[i] if t[i] == NIL then t[i] = nil end end return unpack(t, 1, n) end local function trace(f) return function(...) local t = pack2(f(...)) return unpack2(t) end end local f = trace(function() return 11,12,13,14,15 end) print_bench("nil Placeholder", duration, nbase, ntrials, f, 1,2,3,4,5) end do -- This is a simplified version of Code Generation for comparison. local function tuple(a1,a2,a3,a4,a5) return function() return a1,a2,a3,a4,a5 end end local function trace(f) return function(...) local t = tuple(f(...)) return t() end end local f = trace(function() return 11,12,13,14,15 end) print_bench("Closure", duration, nbase, ntrials, f, 1,2,3,4,5) end do local function build_constructor(n) local t = {}; for i = 1,n do t[i] = "a" .. i end local arglist = table.concat(t, ',') local src = "return function(" .. arglist .. ") return function() return " .. arglist .. " end end" return assert(loadstring(src))() end local cache = {} local function tuple(...) local n = select('#', ...) local construct = cache[n] if not construct then construct = build_constructor(n) cache[n] = construct end return construct(...) end local function trace(f) return function(...) local t = tuple(f(...)) return t() end end local f = trace(function() return 11,12,13,14,15 end) print_bench("Code Generation", duration, nbase, ntrials, f, 1,2,3,4,5) end do local function helper(n, first, ...) if n == 1 then return function() return first end else local rest = helper(n-1, ...) return function() return first, rest() end end end local function tuple(...) local n = select('#', ...) return (n == 0) and function() end or helper(n, ...) end local function trace(f) return function(...) local t = tuple(f(...)) return t() end end local f = trace(function() return 11,12,13,14,15 end) print_bench("Functional, Recursive", duration, nbase, ntrials, f, 1,2,3,4,5) end -- NOTE: Upvalues in C Closure not benchmarked here. print "done" end test_suite(10, 1000000, 3) test_suite(10, 1000000, 1) -- recheck
結果
(Pentium4/3GHz) name, t, t, t (times in sec) (control): 3.8e-007 3.8e-007 4.0e-007 CPS: 5.6e-007 6.3e-007 5.9e-007 Coroutine: 1.7e-006 1.7e-006 1.7e-006 {...} and unpack: 2.2e-006 2.2e-006 2.4e-006 {...} and unpack with n: 2.5e-006 2.5e-006 2.5e-006 nil Placeholder: 5.0e-006 4.7e-006 4.7e-006 Closure: 5.0e-006 5.0e-006 5.0e-006 Code Generation: 5.5e-006 5.5e-006 5.5e-006 Functional, Recursive: 1.3e-005 1.3e-005 1.3e-005 done
CPS 最快,接著是協同程式(兩者都在堆疊上運作)。表格花費比協同程式方法多一點時間,不過如果 resume
上沒有 select
,協同程式可以再快一些。使用閉包的速度慢了好幾倍(即使使用程式碼產生概括化),甚至比使用函數式、遞迴概括化慢好幾個數量級。
如果元組大小為 1,我們取得
name, t, t, t (times in sec) (control): 2.9e-007 2.8e-007 2.7e-007 CPS: 4.3e-007 4.3e-007 4.3e-007 Coroutine: 1.4e-006 1.4e-006 1.4e-006 {...} and unpack: 2.0e-006 2.2e-006 2.2e-006 {...} and unpack with n: 2.4e-006 2.5e-006 2.4e-006 nil Placeholder: 3.3e-006 3.3e-006 3.3e-006 Closure: 2.0e-006 2.0e-006 2.0e-006 Code Generation: 2.2e-006 2.5e-006 2.2e-006 Functional, Recursive: 2.5e-006 2.4e-006 2.2e-006 done
如果元組大小為 20,我們取得
name, t, t, t (times in sec) (control): 8.3e-007 9.1e-007 9.1e-007 CPS: 1.3e-006 1.3e-006 1.1e-006 Coroutine: 2.7e-006 2.7e-006 2.7e-006 {...} and unpack: 3.0e-006 3.2e-006 3.0e-006 {...} and unpack with n: 3.7e-006 3.3e-006 3.7e-006 nil Placeholder: 1.0e-005 1.0e-005 1.0e-005 Closure: 1.8e-005 1.8e-005 1.8e-005 Code Generation: 1.9e-005 1.8e-005 1.9e-005 Functional, Recursive: 5.7e-005 5.7e-005 5.8e-005 done
請注意,表格建置方法的時間差異與元組大小關係不大(因為 建立表格的初期開銷)。相反地,使用閉包所致的執行時間差異會隨著元組大小大幅變化。
問題:已給定兩個變長清單(例如回傳多個值的兩個函式 f
與 g
的回傳值),將這些清單合併成一個單一清單。
由於 Lua 這個行為這個會是一個問題,它會捨棄函數中所有回傳值,除了它是清單中的最後一項。
local function f() return 1,2,3 end local function g() return 4,5,6 end print(f(), g()) -- prints 1 4 5 6
除了將清單轉換成資料物件(例如透過上述第 1 個問題中的方式)這些明顯的解決方案外,還有其他的方法僅使用函數呼叫來執行此操作。
下列方式結合清單遞迴,一次只加上一個元素,並延遲其中一個清單的評估。
local function helper(f, n, a, ...) if n == 0 then return f() end return a, helper(f, n-1, ...) end local function combine(f, ...) local n = select('#', ...) return helper(f, n, ...) end -- TEST local function join(...) local t = {n=select('#', ...), ...} for i=1,t.n do t[i] = tostring(t[i]) end return table.concat(t, ",") end local function f0() return end local function f1() return 1 end local function g1() return 2 end local function f3() return 1,2,3 end local function g3() return 4,5,6 end assert(join(combine(f0, f0())) == "") assert(join(combine(f0, f1())) == "1") assert(join(combine(f1, f0())) == "1") assert(join(combine(g1, f1())) == "1,2") assert(join(combine(g3, f3())) == "1,2,3,4,5,6") print "done"
問題:回傳包含另一個清單中前 N 個元素的清單。
select
函數允許選取清單中的最後 N 個元素,但沒有內建函數可選取前 N 個元素。
local function helper(n, a, ...) if n == 0 then return end return a, helper(n-1, ...) end local function first(k, ...) local n = select('#', ...) return helper(k, ...) end -- TEST local function join(...) local t = {n=select('#', ...), ...} for i=1,t.n do t[i] = tostring(t[i]) end return table.concat(t, ",") end local function f0() return end local function f1() return 1 end local function f8() return 1,2,3,4,5,6,7,8 end assert(join(first(0, f0())) == "") assert(join(first(0, f1())) == "") assert(join(first(1, f1())) == "1") assert(join(first(0, f8())) == "") assert(join(first(1, f8())) == "1") assert(join(first(2, f8())) == "1,2") assert(join(first(8, f8())) == "1,2,3,4,5,6,7,8") print "done"
注意:如果元素的數目是固定的,解決方案會比較簡單。
local function firstthree(a,b,c) return a,b,c end assert(join(firstthree(f8())) == "1,2,3") -- TEST
代碼產生方法可根據此方式為基礎。
問題:將一個元素新增到清單中。
請注意將一個元素前置到清單中很簡單: {a, ...}
local function helper(a, n, b, ...) if n == 0 then return a else return b, helper(a, n-1, ...) end end local function append(a, ...) return helper(a, select('#', ...), ...) end
注意:如果元素的數目是固定的,解決方案會比較簡單。
local function append3(e, a, b, c) return a, b, c, e end
問題:反轉清單。
local function helper(n, a, ...) if n > 0 then return append(a, helper(n-1, ...)) end end local function reverse(...) return helper(select('#', ...), ...) end
注意:如果元素的數目是固定的,解決方案會比較簡單。
local function reverse3(a,b,c) return c,b,a end
問題:在清單上實作 map [3] 函數。
local function helper(f, n, a, ...) if n > 0 then return f(a), helper(f, n-1, ...) end end local function map(f, ...) return helper(f, select('#', ...), ...) end
問題:在清單上實作 filter [4] 函數。
local function helper(f, n, a, ...) if n > 0 then if f(a) then return a, helper(f, n-1, ...) else return helper(f, n-1, ...) end end end local function grep(f, ...) return helper(f, select('#', ...), ...) end
問題:反覆運算可變長參數中的所有元素。
for n=1,select('#',...) do local e = select(n,...) end
如果您不需要 nil 的元素,也可以使用以下方式:
for _, e in ipairs({...}) do -- something with e end
如果您想要使用迭代器函數,而不每次都建立一個垃圾資料表,則可以使用以下方式:
do local i, t, l = 0, {} local function iter(...) i = i + 1 if i > l then return end return i, t[i] end function vararg(...) i = 0 l = select("#", ...) for n = 1, l do t[n] = select(n, ...) end for n = l+1, #t do t[n] = nil end return iter end end for i, v in vararg(1, "a", false, nil) do print(i, v) end -- test -- Output: -- 1 1 -- 2 "a" -- 3 false -- 4 nil
(無)
nil
值和效率有關的限制。