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