Vararg 二等公民

lua-users home
wiki

Lua 變數參數「...[1] 在 Lua 5.1 中不是[一級]物件,這導致表達式中有些限制。以下提供這些問題及解決方法。

問題 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
副作用很好,現在 unpack2 可以處理範圍索引 [k, n],而不是整個表格。如果您未指定範圍,整個表格會被解開。--Sergey Rozhenko,2009,Lua 5.1

另請參閱 StoringNilsInTables

解決方法:延續傳遞樣式 (CPS)

如果我們使用延續傳遞樣式 (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 挑戰]

此外請注意,如果封裝好的函式會引發例外狀況,您會想要另外進行 pcallLuaList: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 Closure 中的上值

元組可以在 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

請注意,表格建置方法的時間差異與元組大小關係不大(因為 建立表格的初期開銷)。相反地,使用閉包所致的執行時間差異會隨著元組大小大幅變化。

問題 2:合併清單

問題:已給定兩個變長清單(例如回傳多個值的兩個函式 fg 的回傳值),將這些清單合併成一個單一清單。

由於 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"

問題 3:選取清單中的前 N 個元素

問題:回傳包含另一個清單中前 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

代碼產生方法可根據此方式為基礎。

問題 4:在清單中加入一個元素

問題:將一個元素新增到清單中。

請注意將一個元素前置到清單中很簡單: {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

問題 5:反轉清單

問題:反轉清單。

解決方案

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

問題 6:map 函式

問題:在清單上實作 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

問題 7:filter 函式

問題:在清單上實作 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

問題 8:疊代變數參數

問題:反覆運算可變長參數中的所有元素。

解決方案

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

其他說明

(無)


--DavidManura,2007 年,Lua 5.1

另請參閱


RecentChanges · 偏好設定
編輯 · 歷史記錄
上次編輯時間為 2017 年 4 月 1 日星期六下午 7:50 GMT (比較)