Lua: Unpack with nil? Alternative?

I've run into the age old unpack bug, where I have an array in Lua that can contain nil values, and I want to unpack the array with the nil values; which seems is not possible. What is the alternative to this logic?

Here is the code I am attempting to run

function InputSystem:poll(name, ...)
  local system = self:findComponent(name)
  local values, arr = {...}, {}
  for i, v in pairs(values) do
    arr[#arr+1] = system[v]
  end

  --If the first guy is null this does not work!! WHY
  return unpack(arr, 1, table.maxn(values))
end

The idea is I poll my input system dynamically so that I only return the values I want like so:

local dragged,clicked,scrolled = self.SystemManager:findSystem('Input'):poll('Mouse', 'Dragged', 'Clicked', 'Scrolled')

Any thoughts? Thanks

EDIT:

I seem to not understand Lua fully. I was wanting to return the same amount of variables as passed in by ..., but in the loop if the property is not found I thought it would set it to nil, but this seems to be wrong.

function InputSystem:poll(name, ...)
      local system = self:findComponent(name)
      local values, arr = {...}, {}
      for i, v in pairs(values) do
        arr[#arr+1] = system[v] --If not found set nil
      end

      --I want this to return the length of ... in variables
      --Example I pass 'Dragged', 'Clicked' I would want it to return nil {x:1,y:1}
      return unpack(arr, 1, table.maxn(values))
    end

Clearly I am a Lua master...

2 answers

  • answered 2018-08-09 00:36 Henri Menke

    You should use table.pack and table.unpack to preserve nils. If you use Lua 5.2 or above you can remove the compatibility snippet.

    -- Backwards compatibility
    table.pack = table.pack or function(...) return { n = select("#", ...), ... } end
    table.unpack = table.unpack or unpack
    
    function test(...)
        local values = table.pack(...)
        local arr = {}
        for i, v in pairs(values) do
            -- iterates only the non-nil fields of "values"
            arr[i] = 10*v
        end
        return table.unpack(arr, 1, values.n)
    end
    
    print(test(nil, 1, nil, 2, nil, nil, 3))
    
    $ lua5.3 test.lua
    nil 10  nil 20  nil nil 30
    $ lua5.2 test.lua
    nil 10  nil 20  nil nil 30
    $ lua5.1 test.lua
    nil 10  nil 20  nil nil 30
    $ luajit test.lua
    nil 10  nil 20  nil nil 30
    

  • answered 2018-08-09 14:06 DarkWiiPlayer

    for i, v in pairs(values) do
      arr[#arr+1] = system[v] -- This doesn't work!
    end
    

    The problem with your implementation is that you expect appending nil to an array to increase it's length, which it doesn't:

    local arr = {1, 2, 3}
    print(#arr) --> 3
    arr[#arr+1]=nil
    print('arr') --> 3
    

    What you want is, essentially, a map function, that takes a list of elements, applies a function fn to each of them and returns the list of the results.

    Normally, this can easily be implemented as a tail-recursive function like this:

    local function map(fn, elem, ...)
      if elem then return fn(elem), map(fn, ...)
    end
    

    This doesn't deal well with nil arguments though, as they would make the condition false while there are still arguments left to handle, but we can modify it using select to avoid this:

    local function map(fn, elem, ...)
      if select('#', ...)>0 then return fn(elem), map(fn, ...)
      else return fn(elem)
    end
    -- This implementation still gets TCOd :)
    

    Then you can use it like this:

    map(string.upper, 'hello', 'world') --> 'HELLO', 'WORLD'
    

    You want to map each value in ... to the corresponding value in a table, but since map takes a function as its first value, we can just wrap that in a function. And because the table isn't known to us at the time of writing the code, we have to generate the function at runtime:

    local function index(table)
      return function(idx)
        return table[idx]
      end
    end
    

    Now we can do this:

    map(index{'hello', 'world'}, 1, 2) --> 'hello', 'world'
    -- index{'hello', 'world'} returns a function that indexes the given table
    -- with its first argument and returns the value
    

    Then you can write your InputSystem function like this:

    function InputSystem:poll(name, ...)
      return map(index(self:findComponent(name)), ...)
    end
    

    Obviously, we don't need that generic map function in this case, since we're always indexing a table. We can rewrite map to use a table like this:

    local function map(tab, elem, ...)
      if select('#', ...)>0 then return tab[elem], map(tab, ...)
      else return tab[elem]
    end
    

    and the main function will become:

    function InputSystem:poll(name, ...)
      return map(self:findComponent(name), ...)
    end
    

    One more thing I noticed:

      for i, v in pairs(values) do
        arr[#arr+1] = system[v] --If not found set nil
      end
    

    pairs iterates out of order, so your line for i, v in pairs(values) do may very well completely re-order the values. Since further down you write local dragged,clicked,scrolled = self.SystemManager:findSystem... I believe you expect the return values to remain in order.