Tips: Working with tables by reference vs. by value

Post Reply
User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Tips: Working with tables by reference vs. by value

by sorcerykid » Post

For those of you familiar with Lua, you undoubtedly know that tables are objects. Therefore, they are always passed by reference to functions. This can lead to unusual errors, however, whenever the table is modified unexpectedly.

Ideally, functions should treat their arguments as immutable and copy by-value every argument that is to be changed to a localized variable -- yes, that includes scalars too. It makes the code easier to debug when the scope and state of arguments is always constant and never in doubt. Of course, this doesn't apply to arguments that are never changed.

1. A simple helper function

Below is a simple helper function that I use extensively in the just_test_tribute subgame. I kind of wish it part of Minetest builtin since it would help to overcome a lot of little bugs, some of which can be very difficult to detect and trace. But, I digress :)
  • t2 = by_value( t1 )
    • t1 - the table to be copied, either a shallow hash or an ordered array
    • t2 - the copied table
This function returns a copy of a table either to be passed as a function argument or inserted into another array or hash. For use with your own mods and subgames, simply copy the following code to builtin/game/misc.lua:

Code: Select all

-----------------------------------------------------------------
-- pass hashes and arrays by value during function calls
-- by sorcerykid (CC0 1.0 Universal)
-----------------------------------------------------------------

function by_value( t1 )
        local t2 = { }
        if #t1 > 0 then
                -- ordered copy of arrays
                t2 = { unpack( t1 ) }
        else
                -- shallow copy of hashes
                for k, v in pairs( t1 ) do t2[ k ] = v end
        end
        return t2
end
2. Passing tables as function arguments.

In this example, we declare a function that takes two arguments, both of which are tables. There is one flaw, however. We are modifying the node table, which is not a behavior that anybody would expect.

Code: Select all

function replace_node( pos, node )
        node.param2 = 0  -- set the rotation to zero
        minetest.swap_node( pos, node )
end
Since, node needs to be changed, we first copy it to a local variable so as to avoid undesired side-effects.

Code: Select all

function replace_node( pos, node )
        node = by_value( node )
        node.param2 = 0  -- set the rotation to zero
        minetest.swap_node( pos, node )
end
Notice that we are conveniently re-using the existing variable, node, for the new table reference. Under most circumstances, this shorthand is acceptable since the re-assignment is occurring at the top of the function. So there is no doubt when reviewing the code that we created a local copy of the argument, to preserve the original from unwanted changes.

If, for some reason you still need access to the original table for other purposes, then you could assign the copied table to a new local variable, such as new_node, thus keeping the argument itself in tact.

3. Inserting tables into arrays or hashes.

Of course function arguments aren't the only cause for concern. Inserting tables within other tables can result in multiple references spanning across a variety of functions and modules. Changes to the original table will no longer be isolated to just that one table. And it will be almost impossible to account for every separate reference to that table should you decide to make a change in only one place! You've heard of the butterfly effect, right? This is most likely not what you are intending.

In this example, we register two nodes with the same groups. However, we want to exclude the second node from the craft guide.

Code: Select all

local unobtanium_groups = {cracky=3, oddly_breakable_by_hand=1}

minetest.register_node("unobtanium:common", {
	description = S("Unobtanium Common Block"),
	tiles = {"unobtanium_common.png"},
	groups = unobtanium_groups,
	sounds = default.node_sound_glass_defaults(),
})

unobtanium_groups.not_in_creative_inventory = 1

minetest.register_node("unobtanium:rare", {
	description = S("Unobtanium Rare Block"),
	tiles = {"unobtanium_rare.png"},
	groups = unobtanium_groups,
	sounds = default.node_sound_glass_defaults(),
})
Notice the mistake. We modified the unobtanium_groups table that is referenced within the node definition for "unobtanium:common", so it too will be removed from the craft guide. Unless you explicitly want a table to be stored by reference, you should always make a copy! Here is a better approach:

Code: Select all

local unobtanium_groups = {cracky=3, oddly_breakable_by_hand=1}

minetest.register_node("unobtanium:common", {
	description = S("Unobtanium Common Block"),
	tiles = {"unobtanium_common.png"},
	groups = by_value(unobtanium_groups),
	sounds = default.node_sound_glass_defaults(),
})

unobtanium_groups.not_in_creative_inventory = 1

minetest.register_node("unobtanium:rare", {
	description = S("Unobtanium Rare Block"),
	tiles = {"unobtanium_rare.png"},
	groups = by_value(unobtanium_groups),
	sounds = default.node_sound_glass_defaults(),
})
In fairness, there are cleaner ways to handle the situation described above. But for sake of simplicity, you can see how easy it is to overlook such small details -- never realizing how they can grow into a much bigger mess if they are not dealt with at the source.

4. Conclusion.

I really hope that this guide helps mod authors to avoid the various snafus that can crop up while working with tables in Lua. Generally speaking, you don't need to go "copy-crazy", since that is an inefficient use of memory. But at the same time, you should still use discretion when inserting tables into arrays or hashes or passing tables as function arguments to avoid the "gotchas" described above.

Thanks for reading!

Byakuren
Member
Posts: 818
Joined: Tue Apr 14, 2015 01:59
GitHub: raymoo
IRC: Hijiri
In-game: Raymoo + Clownpiece

Re: Tips: Working with tables by reference vs. by value

by Byakuren » Post

Nice guide, though I prefer to call this kind of sharing "call by pointer" to distinguish it with the kind of "call by reference" that is possible in C++, which actually aliases variables rather than just passing a thing that points to the same data. I think on wikipedia it is also called "call by sharing".
Every time a mod API is left undocumented, a koala dies.

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: Tips: Working with tables by reference vs. by value

by sorcerykid » Post

Thanks for the feedback. I'm using the term "by reference" merely to be consistent with the conventions of the Progamming in Lua book:

https://www.lua.org/pil/2.5.html

Post Reply

Who is online

Users browsing this forum: No registered users and 11 guests