Last active
August 11, 2024 21:49
-
-
Save tylerneylon/81333721109155b2d244 to your computer and use it in GitHub Desktop.
How to deep copy Lua values.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
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
Thank you @tylerneylon ,
I just have been explained I use Kahlua and not lua.
next is not available in Kahlua.