Skip to content

Instantly share code, notes, and snippets.

@tylerneylon
Last active August 11, 2024 21:49
Show Gist options
  • Save tylerneylon/81333721109155b2d244 to your computer and use it in GitHub Desktop.
Save tylerneylon/81333721109155b2d244 to your computer and use it in GitHub Desktop.
How to deep copy Lua values.
-- 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
@Kristopher38
Copy link

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:

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 an copy recursively.
	local s = seen or {}
	local res = {}
	s[obj] = res
	for k, v in next, obj do res[copy3(k, s)] = copy3(v, s) end
	return setmetatable(res, getmetatable(obj))
end

@tylerneylon
Copy link
Author

Thanks, @mnemnion and @Kristopher38 for the useful feedback! I'll update the snippet based on this.

@tim99-1977
Copy link

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

@DavidLaRoi
Copy link

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

@katupia
Copy link

katupia commented May 6, 2023

Hi, @mnemnion @Kristopher38
What is the next command ?
I have no access to it.

@tylerneylon
Copy link
Author

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

@katupia
Copy link

katupia commented May 7, 2023

Thank you @tylerneylon ,
I just have been explained I use Kahlua and not lua.
next is not available in Kahlua.

@minimapletinytools
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment