Spreading tables in Lua
published:
modified:
categories: open-source
Javascript has a spreading operator which lets us splice the contents of an object or array into another object or array. This makes it very easy to create an object based on another object and override or add entries. Since Lua and Javascript are quite similar, wouldn't it be nice to have this operator in Lua as well? Lua is a minimal language, so adding a new operator seems unlikely, but Lua is also very flexible, and we can add a spreading function instead.
Before we go on, let's first see how spreading works in Javascript. Assume we want to make a game which keeps track of players as objects. We want to have a default player template and then create new players based on that template.
// The template, never used as an actual player
const default_player = {
name: '',
score: 0,
position: {
x: 0,
y: 0
}
};
// An actual player, we override the name
const player1 = {
...default_player,
name: 'Alice'
};
A naive spread function
We will use Lua's support for closures and the ability to return a function. Here is the implementation of the spread function:
local function spread(template)
local result = {}
for key, value in pairs(template) do
result[key] = value
end
return function(table)
for key, value in pairs(table) do
result[key] = value
end
return result
end
end
Let's take it apart. The function takes one argument, the original table we want to spread apart. The return value is another function which also takes in a table and returns a new table. The signature looks something like this:
spread: Table -> (Table -> Table)
That's a lot of tables, so what is going on here? Here are the steps performed by the source code:
Create a new table called
result
. This will be the final result, but it is still empty at this point.Copy all the values from the
template
table into it. Now theresult
is just a copy of thetemplate
.Return a closure. The closure has a reference to the
result
, so we can still read and write to that table.The closure takes in a new
table
and overwrites the entries ofresult
with entries oftable
.At this point the
result
is done and we can return it.
You might be wondering why I chose the convoluted approach of returning a closure instead of simply taking two arguments. Indeed, that would have been easier to write, but more awkward to use. This can be seen when we translate the above Javascript example to Lua.
local default_player = {
name = '',
score = 0,
position = {
x = 0,
y = 0,
}
}
local player1 = spread(default_player) {
name = 'Alice'
}
The first statement is almost the same as the Javascript version. In the second
statement I make use of the fact that in Lua if the only argument to a function
is a table literal we can omit the parentheses around the argument. This lets
us write the statement in a very elegant way. The expression spread(...)
almost looks like an operator, but since it's a function we can also assign it
to a variable.
-- A function which takes a table and returns a table
local player = spread(default_player)
local player2 = player {
name = 'Bob'
}
local player3 = player {
name = 'Carol',
score = 3
}
This looks very declarative, and that is no coincidence. Lua's direct ancestor Sol was a language for describing static data and Lua was created with the intention to be used as a cross between a data description language and a programming language (see the history of Lua). This declarative syntax is a natural match. It is something we could safely expose inside a sandbox for user's to declare their data.
Further improvements
The naive implementation works for the most part and fully encapsulated the core idea, but there a few details that should be fixed.
Beware of shared closures
As the code now stands, if we want to re-use the closure we will have one shared table. Consider the following example:
local player = spread(default_player)
local player1 = player {
name = 'Alice',
score = 5,
}
local player2 = player {
name = 'Bob',
}
Bob will inherit the score of Alice because they both share the same intermediate closure, and thus the same intermediate result. Even worse, Bob will retroactively overwrite Alice's name as well. We can solve this by spreading the template into a new table each time.
local function spread(template)
return function(table)
local result = {}
for key, value in pairs(template) do
result[key] = value
end
for key, value in pairs(table) do
result[key] = value
end
return result
end
end
Now the outer function is nothing more than just a wrapper.
Deep copying of tables
Copying scalar entries is simple, but what if an entry has a table as its value, such as the player's position? Since tables in Lua are passed by reference changing one table will affect all other tables as well:
local player1 = player { name = 'Alice' }
-- This will mutate the shared position table
player1.position.x = 1
-- Now Bob and Carol also have their position.x set to 1
local player2 = player { name = 'Bob' }
local player3 = player { name = 'Carol', }
In that case we need to perform a deep copy on the value. This ensures that each player has their own separate copy of the position. Here is the code:
local function deep_copy(object)
if type(object) ~= 'table' then return object end
local result = {}
for key, value in pairs(object) do
result[key] = deep_copy(value)
end
return result
end
local function spread(template)
return function(table)
local result = {}
for key, value in pairs(template) do
result[key] = deep_copy(value) -- Note the deep copy!
end
for key, value in pairs(table) do
result[key] = value
end
return result
end
end
This takes care of tables in the template, but there is still the problem of tables in the new value. If we assign a table to a value it will overwrite the previous value. But what if the previous value was a table and we wanted only to overwrite certain entries?
local player1 = player {
name = 'Alice',
-- Adjust the X position, but keep the Y position
position = {
x = 5
}
}
Unfortunately there is no universal answer to this question. Do we really want
to merge the two tables, or do we want to overwrite the old table? I think it
this case it is more consistent and predictable to have the value be
overwritten (follow the principle of least astonishment). If users really want
to merge, they can use our spread
function.
-- A new spreader function
local position = spread(default_player.position)
local player1 = player {
name = 'Alice',
position = position {
x = 5
}
}
Metatables
The template table can have an associated metatable which we want new instances to inherit.
local function spread(template)
return function(table)
local mt = getmetatable(template)
local result = {}
setmetatable(result, mt)
for key, value in pairs(template) do
result[key] = deep_copy(value)
end
for key, value in pairs(table) do
result[key] = value
end
return result
end
end
If the template
has no metatable nothing will happen.
Skip the intermediate closure
Returning a closure from spread
is elegant if we are dealing with a table
literal, but it gets rather ugly if we have a table variable.
local player1_settings = { name = 'Alice' }
local player1 = spread(default_player)(player1_settings)
-- What I would rather want to write:
local player1 = spread(default_player, player1_settings)
It's not that ugly, but it would be more natural if we could simply pass the
second table as a second argument to the spread
function instead. We can
reverse-curry the spread
function by using the fact that missing arguments in
Lua get assigned nil
.
local function spread(template, override)
-- This is now a variable, not the return value
local splice = function(override)
local mt = getmetatable(template)
local result = {}
setmetatable(result, mt)
for key, value in pairs(template) do
result[key] = deep_copy(value)
end
for key, value in pairs(override) do
result[key] = value
end
return result
end
-- Using the '_ and _ or _' pattern as a ternary operator
return override and splice(override) or splice
end
If the override
argument is not nil
we immediately splice it into the
result. Otherwise we return the same function as before. We create a closure
each time, so here is a variant which only creates a closure when needed:
local function spread(template, override)
if not override then
return function(override)
spread(template, override)
end
end
local mt = getmetatable(template)
local result = {}
setmetatable(result, mt)
for key, value in pairs(template) do
result[key] = deep_copy(value)
end
-- No longer wrapped up inside a function
for key, value in pairs(override) do
result[key] = value
end
return result
end
This variant only creates a closure if the second argument is missing. This may or may not be more efficient, I have not tried it. I leave it as an exercise to the reader.
Conclusion
Using Lua's powerful mechanisms (first-class functions and closures) and its convenient syntax for table literals we have built a simple first spreading function. With the basic idea in place we were then able to chip away at the more obscure issues one at a time.
Note however, that a spreading function is not necessarily the best way of
implementing default values in Lua tables. The goal of this article has been to
implement a Lua analogue of the spread operator in Javascript. In practice
though you would more likely use a metatable that implements the __index
metamethod. It is generally better the use what the language already provides,
but if you need truly separate table instances, then spreading is a solution.
Update
It has been brought to my attention that the original implementation would
keep mutating the shared result
table if the closure gets re-used multiple
times. I have left this bug in the naive implementation, but added an
improvement to address the issue.