Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
@mnemnion

This comment has been minimized.

Copy link

@mnemnion mnemnion commented Jan 15, 2020

You'll want to set the metatable after copying the data, otherwise you risk triggering __index and __newindex metamethods, which will change the semantics.

Similarly, better to invoke the for loop with for k, v in next, obj do, to avoid triggering a __pairs metamethod.

@Kristopher38

This comment has been minimized.

Copy link

@Kristopher38 Kristopher38 commented Apr 20, 2020

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

This comment has been minimized.

Copy link
Owner Author

@tylerneylon tylerneylon commented Apr 21, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.