Vararg The Second Class Citizen

lua-users home
wiki

Lua varargs "..." [1] are not [first class] objects in Lua 5.1, and this leads to some limitations in expression. Some of these issues and their workarounds are given here.

Issue #1: Vararg Saving

Lua 5.1 vararg (...) handling is a bit limited. For example, it doesn't permit things like this:

function tuple(...) return function() return ... end end
--Gives error "cannot use '...' outside a vararg function near '...'"

(Some comments on that are in LuaList:2007-03/msg00249.html .)

You might want to use such a function to temporarily store away the return values of a function call, do some other work, and then retrieve those stored return values again. The following function would use this hypothetical tuple function to add trace statements around a given function:

--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

Still, there are ways to achieve this in Lua.

Solution: {...} and unpack

You could instead implement trace with the table construct {...} and unpack:

--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

Unfortunately, it misses a nil return value since nil are not explicitly storable in tables , and particularly {...} does not preserve information about trailing nils (this is further discussed in StoringNilsInTables).

Solution: {...} and unpack with n

The following improvement on the previous solution properly handles nils:

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

A variant noted by Shirik is

local function tuple(...)
  local n = select('#', ...)
  local t = {...}
  return function() return unpack(t, 1, n) end
end

Solution: nil Placeholders

The following approach swaps the nil's with placeholders that can be stored in tables. It's probably less optimal here, but the approach might be usable elsewhere.

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

Here are more optimal implementations of pack2 and 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
As a good side effect, now unpack2 can operate on range of indexes [k, n] instead of whole table. If you don't specify the range, whole table is unpacked.--Sergey Rozhenko, 2009, Lua 5.1

See also StoringNilsInTables.

Solution: Continuation Passing Style (CPS)

Tables can be avoided if we use the Continuation passing style (CPS) ([Wikipedia]) as below. We could expect this to be a bit more efficient.

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

The CPS approach was also used in the RiciLake's string split function (LuaList:2006-12/msg00414.html).

Solution: Code Generation

Another approach is code generation, which compiles a separate constructor for each tuple size. There is some initial overhead building the constructors, but the constructors themselves can be optimally implemented. The tuple function used previously can be implemented as such:

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

To avoid the overhead of code generation on each store, we can memoize the make_storeimpl function (for background see [Wikipedia:Memoization] and 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)

A more complete example of tuples implemented via code generation is in FunctionalTuples.

The code building/memoization technique and the above Memoize function are based on some previous related examples by RiciLake such as [Re: The Curry Challenge].

Note also that if your wrapped function raises exceptions, you would want to pcall as well (LuaList:2007-02/msg00165.html).

Solution: Functional, Recursive

The following approach is purely functional (no tables) and avoids code generation. It's not necessarily the most efficient as it creates a function per tuple element.

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"

Solution: Coroutines

Another idea is with coroutines:

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

A similar suggestion was posted in LuaList:2007-02/msg00142.html . That can be inefficient though (RiciLake notes that a minimal coroutine occupies slightly more than 1k plus malloc overhead, on freebsd it totals close to 2k, and the largest part is the stack, which defaults to 45 slots @ 12 or 16 bytes each).

It is not necessary to create a new coroutine on each call. The following approach is rather efficient, and the recursion uses a tail call:

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

Solution: Upvalues in C Closure

Tuples can be implemented in C as a closure containing the tuple elements as upvalues. This is demonstrated in Section 27.3 of Programming In Lua, 2nd Ed [2].

Benchmarks

The speeds of the above solutions are compared in the following benchmark.

-- 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

Results:

(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

The CPS is the fastest, followed by coroutines (both operated on the stack). Tables take a bit more time than the coroutine approach, though coroutines could be even faster if we didn't have the the select on the resume. Use of closures are a few times slower still (including when generalized with code generation) to an order of magnitude slower (if generalized with Functional, Recursive).

For a tuple size of 1, we get

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

For a tuple size of 20, we get

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

Notice that the times for table construction methods differ relatively little with respect to tuple size (due to the initial overhead of constructing a table). In contrast, use of closures entails run times that vary more significantly with tuple size.

Issue #2: Combining Lists

Problem: given two variable length lists (e.g. the return values of two functions, f and g, that each return multiple values), combine them into a single list.

This can be a problem because of the behavior of Lua to discard all but the first return value of a function unless it is the last item in a list:

local function f() return 1,2,3 end
local function g() return 4,5,6 end
print(f(), g()) -- prints 1 4 5 6

Besides the obvious solutions of converting the lists into objects such as tables (via the methods in Issue #1 above), there are ways to do this with only function calls.

Solution

The following combines lists recursively by prepending only one element at a time and delaying evaluation of one of the lists.

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"

Issue #3: Selecting the First N Elements in List

Problem: Return a list consisting of the first N elements in another list.

The select function allows selecting the last N elements in a list, but there is no built-in function for selecting the first N elements.

Solution

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"

Note: if the number of elements is fixed, the solution is easier:

local function firstthree(a,b,c) return a,b,c end
assert(join(firstthree(f8())) == "1,2,3")  -- TEST

Code generation approaches can be based on this.

Issue #4: Appending One Element to a List

Problem: Append one element to a list.

Note that prepending one element to a list is simple: {a, ...}

Solution

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

Note: if the number of elements is fixed, the solution is easier:

local function append3(e, a, b, c) return a, b, c, e end

Issue #5: Reversing a List

Problem: Reverse a list.

Solution

local function helper(n, a, ...)
  if n > 0 then return append(a, helper(n-1, ...)) end
end
local function reverse(...)
  return helper(select('#', ...), ...)
end

Note: if the number of elements is fixed, the solution is easier:

local function reverse3(a,b,c) return c,b,a end

Issue #6: The map Function

Problem: Implement the map [3] function over a list.

Solution

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

Issue #7: The filter Function

Problem: Implement the filter [4] function over a list.

Solution

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

Issue #8: Iterating over Varargs

Problem: Iterate over all elements in the vararg.

Solution

for n=1,select('#',...) do
  local e = select(n,...)
end

If you do not need nil elements, you can also use the following:

for _, e in ipairs({...}) do
   -- something with e
end

If you wish to use an iterator function without creating a garbage table every time, you can use the following:

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

Other Comments

(none)


--DavidManura, 2007, Lua 5.1

See Also


RecentChanges · preferences
edit · history
Last edited April 1, 2017 7:50 pm GMT (diff)