-
-
Save tylerneylon/81333721109155b2d244 to your computer and use it in GitHub Desktop.
-- copy.lua | |
-- | |
-- Lua functions of varying complexity to deep copy tables. | |
-- | |
-- 1. The Problem. | |
-- | |
-- Here's an example to see why deep copies are useful. Let's | |
-- say function f receives a table parameter t, and it wants to | |
-- locally modify that table without affecting the caller. | |
-- This code fails: | |
-- | |
-- function f(t) | |
-- t.a = 3 | |
-- end | |
-- | |
-- local my_t = {a = 5} | |
-- f(my_t) | |
-- print(my_t.a) --> 3 | |
-- | |
-- This behavior can be hard to work with because, in general, | |
-- side effects such as input modifications make it more | |
-- difficult to reason about program behavior. | |
-- 2. The easy solution. | |
function copy1(obj) | |
if type(obj) ~= 'table' then return obj end | |
local res = {} | |
for k, v in pairs(obj) do res[copy1(k)] = copy1(v) end | |
return res | |
end | |
-- This functions works well for simple tables. Since it is a | |
-- clear, concise function, and since I most often work with | |
-- simple tables, this is my favorite version. | |
-- | |
-- There are two aspects this does not handle: | |
-- * metatables | |
-- * recursive tables | |
-- 3. Adding metatable support. | |
function copy2(obj) | |
if type(obj) ~= 'table' then return obj end | |
local res = setmetatable({}, getmetatable(obj)) | |
for k, v in pairs(obj) do res[copy2(k)] = copy2(v) end | |
return res | |
end | |
-- Well, that wasn't so hard. | |
-- 4. Supporting recursive structures. | |
-- | |
-- The issue here is that the following code will call itself | |
-- indefinitely and ultimately cause a stack overflow: | |
-- | |
-- local my_t = {} | |
-- my_t.a = my_t | |
-- local t_copy = copy2(my_t) | |
-- | |
-- This happens to both copy1 and copy2, which each try to make | |
-- a copy of my_t.a, which involves making a copy of my_t.a.a, | |
-- which involves making a copy of my_t.a.a.a, etc. The | |
-- recursive table my_t is perfectly legal, and it's possible to | |
-- make a deep_copy function that can handle this by tracking | |
-- which tables it has already started to copy. | |
-- | |
-- Thanks to @mnemnion for pointing out that we should not call | |
-- setmetatable() until we're doing copying values; otherwise we | |
-- may accidentally trigger a custom __index() or __newindex()! | |
function copy3(obj, seen) | |
-- Handle non-tables and previously-seen tables. | |
if type(obj) ~= 'table' then return obj end | |
if seen and seen[obj] then return seen[obj] end | |
-- New table; mark it as seen and copy recursively. | |
local s = seen or {} | |
local res = {} | |
s[obj] = res | |
for k, v in pairs(obj) do res[copy3(k, s)] = copy3(v, s) end | |
return setmetatable(res, getmetatable(obj)) | |
end |
Thanks, @mnemnion and @Kristopher38 for the useful feedback! I'll update the snippet based on this.
just starting with Lua, so apologize if the question is stupid... but whats wrong with this (noone mentions this in my google search, so it's quite obviously not correct, but cant figure out why):
--random table to dupe
local source_t = {
[1] = {field_1 = "abc", field_2 = 123},
[2] = {field_1 = "def", field_2 = 456},
[3] = {field_1 = "ghi", field_2 = 789}
}
--whats wrong with this?
local dest_t = {}
dest_t = source_t
Hi @tim99-1977, not a stupid question.
After your last line, dest_t now references the same table as source_t. This means that any alterations made to source_t are also made to dest_t.
If you make a copy of a table, then changes made to the copy don't affect the source table (or vice versa).
Hi, @mnemnion @Kristopher38
What is the next command ?
I have no access to it.
@katupia They're referring to the next()
function, which is a primitive (built-in) in Lua. You might want to use it in copy3()
to handle the case when obj
(or one of its sub-elements) has overridden the __pairs
metamethod.
More info about how that works is on this page about stateless iterators.
I've left out that detail in copy3()
because, in my experience, it's rare to override the __pairs
metamethod, though I have seen the __index
and __newindex
metamethods be overridden. One of my goals in writing the code above is to keep it readable by most Lua programmers, which is sometimes at odds with handling every possible case. I think the above code is (relatively) clear and handles the vast majority of all use cases.
Thank you @tylerneylon ,
I just have been explained I use Kahlua and not lua.
next is not available in Kahlua.
Here's a luau compatible version of copy3
function deep_copy(obj : any, seen : ({ [any]: {} })?)
-- Handle non-tables and previously-seen tables.
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
-- New table; mark it as seen and copy recursively.
local s = seen or ({} :: { [any]: {} })
local res = {}
s[obj] = res
for k, v in pairs(obj) do res[deep_copy(k, s)] = deep_copy(v, s) end
return setmetatable(res, getmetatable(obj))
end
Can confirm that this is the case (you might run into issues when you're doing classes overriding
__index
and__newindex
metamethods, and trying to deepcopy your objects), here is the updated code that correctly copies tables with__index
and__newindex
metamethods according to the tips by @mnemnion: