From 5fa80ca97cd68d4a0a928d9ed15466f4c935b643 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 2 Mar 2022 08:11:57 -0800 Subject: [PATCH] tablex: Add deep & shallow for copy, overlay Replace copy with shallow_copy, deep_copy. Replace overlay with shallow_overlay, deep_overlay. Significant rewrite of these functions and their semantics to update to the note above them. Add corresponding tests. --- init.lua | 8 ++-- sequence.lua | 6 ++- tablex.lua | 112 ++++++++++++++++++++++++++++++--------------------- tests.lua | 73 +++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 52 deletions(-) diff --git a/init.lua b/init.lua index 08d3a1e..323eb81 100644 --- a/init.lua +++ b/init.lua @@ -64,16 +64,16 @@ function _batteries:export() end --overlay tablex and functional and sort routines onto table - self.tablex.overlay(table, self.tablex) + self.tablex.shallow_overlay(table, self.tablex) --now we can use it through table directly - table.overlay(table, self.functional) + table.shallow_overlay(table, self.functional) self.sort:export() --overlay onto global math table - table.overlay(math, self.mathx) + table.shallow_overlay(math, self.mathx) --overlay onto string - table.overlay(string, self.stringx) + table.shallow_overlay(string, self.stringx) --overwrite assert wholesale (it's compatible) assert = self.assert diff --git a/sequence.lua b/sequence.lua index 99374ce..4277a4a 100644 --- a/sequence.lua +++ b/sequence.lua @@ -44,8 +44,10 @@ for _, v in ipairs({ "dedupe", "collapse", "append", - "overlay", - "copy", + "shallow_overlay", + "deep_overlay", + "shallow_copy", + "deep_copy", }) do local table_f = table[v] sequence[v] = function(self, ...) diff --git a/tablex.lua b/tablex.lua index 4611c6a..38de632 100644 --- a/tablex.lua +++ b/tablex.lua @@ -328,61 +328,81 @@ if not tablex.clear then end end ---note: --- copies and overlays are currently not satisfactory --- --- i feel that copy especially tries to do too much and --- probably they should be split into separate functions --- to be both more explicit and performant, ie --- --- shallow_copy, deep_copy, shallow_overlay, deep_overlay --- --- input is welcome on this :) - ---copy a table --- deep_or_into is either: --- a boolean value, used as deep flag directly --- or a table to copy into, which implies a deep copy --- if deep specified: --- calls copy method of member directly if it exists --- and recurses into all "normal" table children --- if into specified, copies into that table --- but doesn't clear anything out --- (useful for deep overlays and avoiding garbage) -function tablex.copy(t, deep_or_into) +-- Copy a table +-- See shallow_overlay to shallow copy into another table to avoid garbage. +function tablex.shallow_copy(t) assert:type(t, "table", "tablex.copy - t", 1) - local is_bool = type(deep_or_into) == "boolean" - local is_table = type(deep_or_into) == "table" - - local deep = is_bool and deep_or_into or is_table - local into = is_table and deep_or_into or {} + local into = {} for k, v in pairs(t) do - if deep and type(v) == "table" then - if type(v.copy) == "function" then - v = v:copy() - else - v = tablex.copy(v, deep) - end - end into[k] = v end return into end ---overlay tables directly onto one another, shallow only ---takes as many tables as required, ---overlays them in passed order onto the first, ---and returns the first table with the overlay(s) applied -function tablex.overlay(a, b, ...) - assert:type(a, "table", "tablex.overlay - a", 1) - assert:type(b, "table", "tablex.overlay - b", 1) - for k,v in pairs(b) do - a[k] = v +local function deep_copy(t, copied) + -- TODO: consider supporting deep_copy(3) so you can always use deep_copy without type checking + local into = {} + for k, v in pairs(t) do + local clone = v + if type(v) == "table" then + if copied[v] then + clone = copied[v] + elseif type(v.copy) == "function" then + clone = v:copy() + assert:type(clone, "table", "copy() didn't return a copy") + else + clone = deep_copy(v, copied) + setmetatable(clone, getmetatable(v)) + end + copied[v] = clone + end + into[k] = clone end - if ... then - return tablex.overlay(a, ...) + return into +end +-- Recursively copy values of a table. +-- Retains the same keys as original table -- they're not cloned. +function tablex.deep_copy(t) + assert:type(t, "table", "tablex.deep_copy - t", 1) + return deep_copy(t, {}) +end + +-- Overlay tables directly onto one another, merging them together. +-- Doesn't merge tables within. +-- Takes as many tables as required, +-- overlays them in passed order onto the first, +-- and returns the first table. +function tablex.shallow_overlay(dest, ...) + assert:type(dest, "table", "tablex.shallow_overlay - dest", 1) + for i = 1, select("#", ...) do + local t = select(i, ...) + assert:type(t, "table", "tablex.shallow_overlay - ...", 1) + for k,v in pairs(t) do + dest[k] = v + end end - return a + return dest +end + +-- Overlay tables directly onto one another, merging them together into something like a union. +-- Also overlays nested tables, but doesn't clone them (so a nested table may be added to dest). +-- Takes as many tables as required, +-- overlays them in passed order onto the first, +-- and returns the first table. +function tablex.deep_overlay(dest, ...) + assert:type(dest, "table", "tablex.deep_overlay - dest", 1) + for i = 1, select("#", ...) do + local t = select(i, ...) + assert:type(t, "table", "tablex.deep_overlay - ...", 1) + for k,v in pairs(t) do + if type(v) == "table" and type(dest[k]) == "table" then + tablex.deep_overlay(dest[k], v) + else + dest[k] = v + end + end + end + return dest end --collapse the first level of a table into a new table of reduced dimensionality diff --git a/tests.lua b/tests.lua index e45f4a0..109deb4 100644 --- a/tests.lua +++ b/tests.lua @@ -9,6 +9,79 @@ local tablex = require("batteries.tablex") -- tablex {{{ +local function test_shallow_copy() + local x,r + x = { a = 1, b = 2, c = 3 } + r = tablex.shallow_copy(x) + assert:equal(r.a, 1) + assert:equal(r.b, 2) + assert:equal(r.c, 3) + + x = { a = { b = { 2 }, c = { 3 }, } } + r = tablex.shallow_copy(x) + assert:equal(r.a, x.a) +end + +local function test_deep_copy() + local x,r + x = { a = 1, b = 2, c = 3 } + r = tablex.deep_copy(x) + assert:equal(r.a, 1) + assert:equal(r.b, 2) + assert:equal(r.c, 3) + + x = { a = { b = { 2 }, c = { 3 }, } } + r = tablex.deep_copy(x) + assert(r.a ~= x.a) + assert:equal(r.a.b[1], 2) + assert:equal(r.a.c[1], 3) +end + + +local function test_shallow_overlay() + local x,y,r + x = { a = 1, b = 2, c = 3 } + y = { c = 8, d = 9 } + r = tablex.shallow_overlay(x, y) + assert( + tablex.deep_equal( + r, + { a = 1, b = 2, c = 8, d = 9 } + ) + ) + + x = { b = { 2 }, c = { 3 }, } + y = { c = { 8 }, d = { 9 }, } + r = tablex.shallow_overlay(x, y) + assert(r.b == x.b) + assert(r.c == y.c) + assert(r.d == y.d) + assert( + tablex.deep_equal( + r, + { b = { 2 }, c = { 8 }, d = { 9 }, })) +end + +local function test_deep_overlay() + local x,y,r + x = { a = 1, b = 2, c = 3 } + y = { c = 8, d = 9 } + r = tablex.deep_overlay(x, y) + assert( + tablex.deep_equal( + r, + { a = 1, b = 2, c = 8, d = 9 })) + + x = { a = { b = { 2 }, c = { 3 }, } } + y = { a = { c = { 8 }, d = { 9 }, } } + r = tablex.deep_overlay(x, y) + assert( + tablex.deep_equal( + r, + { a = { b = { 2 }, c = { 8 }, d = { 9 }, } })) +end + + local function test_shallow_equal() local x,y x = { a = { b = { 2 }, } }