GrizzlyEnglish
GrizzlyEnglish

Reputation: 131

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

Upvotes: 0

Views: 4504

Answers (2)

DarkWiiPlayer
DarkWiiPlayer

Reputation: 7064

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

Upvotes: 0

Henri Menke
Henri Menke

Reputation: 10939

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

Upvotes: 3

Related Questions