1

Assume I have a function that returns multiple values. I happen to be working with LÖVE's (Image):getDimensions. This returns two values, which I know to be width,height. I want to assign them to a new table, as an array. I would like named (string) keys. So for instance, I would like to assign the return values of the getDimensions() function to a new table with keys width and height, respectively.

I know the following works...

image = {}
image.data = love.graphics.newImage('myimage.png')
image.size = {}
image.size.width, image.size.height = image.data:getDimensions()

I'm wondering if there is any sort of syntactic sugar I can use, or any use of standard library functions that will allow a syntax more along the lines of...

image.size = { width, height = image.data:getDimensions() }

I know the above line does not work, along with many variations I've tried, including various attempts to use unpack(). I'm new to Lua (~2 days in now), so maybe there is another standard function or best practice that I'm unaware of that will associate a table of keys to an array-like table. Thanks!

2
  • You could do image.size = table.pack(image.data:getDimensions()), but that would create image.size[1] and image.size[2], i.e. make image.size a table with integer keys. That is probably not what you want. Commented Jan 1, 2017 at 21:25
  • @MarcBalmer Thank you Marc, but that is what I'm trying to avoid. I want to be able to reference the elements by a specific name later, such as width and height. It seems I wouldn't even need to use table.pack(), as I could just wrap the function call in curly brackets (which might implicitly do the same thing as table.pack()), like so image.size = { image.data:getDimensions() }. I'm currently thinking what I actually want would be accomplished with a class-like construct using a helper function, and a __metatable with a definition for __newindex. Commented Jan 1, 2017 at 21:42

2 Answers 2

4

You can write your own functions:

local function set_fields(tab, fields, ...)
   -- fields is an array of field names
   -- (use empty string to skip value at corresponging position)
   local values = {...}
   for k, field in ipairs(fields) do
      if field ~= "" then 
         tab[field] = values[k]
      end
   end
   return tab
end

local function get_fields(tab, fields)
   local values = {}
   for k, field in ipairs(fields) do
      values[k] = tab[field]
   end
   return (table.unpack or unpack)(values, 1, #fields)
end

Usage example #1:

image.size = set_fields({}, {"width", "height"}, image.data:getDimensions())

Usage example #2:
Swap the values on-the-fly!

local function get_weekdays()
   return "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
end
-- we want to save returned values in different order
local weekdays = set_fields({}, {7,1,2,3,4,5,6}, get_weekdays())
-- now weekdays contains {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}

Usage example #3:

local function get_coords_x_y_z()
   return 111, 222, 333  -- x, y, z of the point
end
-- we want to get the projection of the point on the ground plane
local projection = {y = 0}
-- projection.y will be preserved, projection.x and projection.z will be modified
set_fields(projection, {"x", "", "z"}, get_coords_x_y_z())
-- now projection contains {x = 111, y = 0, z = 333}

Usage example #4:
If require("some_module") returns a module with plenty of functions inside, but you need only a few of them:

local bnot, band, bor = get_fields(require("bit"), {"bnot", "band", "bor"})
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you Egor. I had been considering this approach, and like it's general applicability.
0

Using a class construct, I have come up with the following...

Size = {}    
Size.mt = {}
Size.prototype = { width = 0, height = 0 }

function Size.new (dimensions)
  local size = setmetatable({}, Size.mt)

  if dimensions ~= nil and type(dimensions) == 'table' then
    for k,v in pairs(dimensions) do    
      size[k] = v
    end
  end

  return size
end

Size.mt.__index = function (t, k)
  if k == 'width' or k == 'height' then
    rawset(t, k, Size.prototype[k])

    return t[k]
  else
    return nil
  end
end

Size.mt.__newindex = function (t, k, v)
  if k == 1 or k == 'width' then
    rawset(t, 'width', v)
  elseif k == 2 or k == 'height' then
    rawset(t, 'height', v)
  end
end

Then I can initialize a Size object in a number of ways

  • Using multiple return values:
    • image.size = Size.new{image.data:getDimensions()}
    • image.size = Size.new(table.pack(image.data:getDimensions())
  • Using default values:
    • image.size = Size.new()
    • image.size = Size.new{}
    • image.size = Size.new({})
  • Using mixed array and hash tables:
    • image.size = Size.new({height=20, width=30})
    • image.size = Size.new({height=20, 30})

There are pros and cons to this approach vs. Egor's (utility function), which is what I was considering doing if there wasn't a simple syntax trick or an existing function that I was unaware of.

Pros:

  • (personal) learning experience with OO constructs in Lua
  • I can limit the number of actual keys on the table, while allowing 'synonyms' for those keys to be added by expanding the accepted values in the if/else logic of __index and __newindex
  • Explicit definition of fields in the table, without needing to worry about syncing a table of keys with a table of values (as with a general purpose utility function)

Cons

  • would need to repeat this pattern for each data structure where I wanted this behavior
  • costly, a lot of overhead for what amounts to a very small difference to the consumer

I'm sure I can make this approach more robust in time, but I would appreciate any feedback.

2 Comments

This is OK: Size.new({height=20, 30}), but the opposite Size.new({width=30, 20}) is not OK.
@EgorSkriptunoff good point. I could argue for it being desired behavior, or some additional logic along the lines of if rawget('width', 1) ~= nil could be added to if k == 1 ... in __newindex. This would then have to be extended to checking rawget('height', 2) when k == 2, and then continue to discard any additional array keys. This method doesn't scale very well with more desired fields in the table. Although I would argue it is poor use of the class, passing mixed tables becomes less user friendly as well I'm liking the general purpose utility functions more and more :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.