Automagic Tables |
|
a = AutomagicTable()
a.b.c.d = "a.b and a.b.c are automatically created"
This version was the result of several interesting interchanges between ThomasWrensch and me (RiciLake), and I cannot take credit for it (although I am happy to take the blame; I've made a couple of modifications and changed the formatting because I like spaces in parameter lists)
do local auto, assign function auto(tab, key) return setmetatable({}, { __index = auto, __newindex = assign, parent = tab, key = key }) end local meta = {__index = auto} -- The if statement below prevents the table from being -- created if the value assigned is nil. This is, I think, -- technically correct but it might be desirable to use -- assignment to nil to force a table into existence. function assign(tab, key, val) -- if val ~= nil then local oldmt = getmetatable(tab) oldmt.parent[oldmt.key] = tab setmetatable(tab, meta) tab[key] = val -- end end function AutomagicTable() return setmetatable({}, meta) end end
I think the technique above -- the use of an individualized metatable with additional non-metamethod keys -- is more than just a handy trick.
If you want to associate some information with a table you have three choices:
The problem with the first trick is it may conflict with an existing key and/or pollute the table with inappropriate data. You pointed out some of the problems with the second approach in your message (on GarbageCollectingWeakTables) a while ago. The third requires the use of an individual metatable, but does not have the problems of the other two techniques.
Another implementation, with a limited depth for generating tables;
-- index function to do the magic local autotable__index = function(self, key) local mt = getmetatable(self) local t = {} if mt.depth ~= 1 then setmetatable(t, { __index = mt.__index, depth = mt.depth - 1}) end self[key] = t return t end --- Creates a new auto-table. -- @param depth (optional, default 1) how deep to auto-generate tables. The last -- table in the chain generated will itself not be an auto-table. If `depth == 0` then -- there is no limit. -- @return new auto-table function newautotable(depth) return setmetatable({}, {__index = autotable__index, depth = depth }) end --- Checks a table to be an auto-table -- @param t table to check -- @return `true` if `t` is an auto-table, `false` otherwise function isautotable(t) if type(t) ~= "table" then return false end return ((getmetatable(t) or {}).__index == autotable__index) end
And some tests to verify (how) it works
local deeptest = newautotable(0) local x = deeptest for n = 1, 100 do x = x.a end x.final = "100 levels deep" x = deeptest for n = 1, 100 do x = x.a end print("Created:",x.final) local testtable = newautotable(2) print(testtable.a.b) -- depth == 2, so a and b are autogenerated print(testtable.a.b) -- but b is last level and no longer auto-table itself testtable.a.b.hello = "world" testtable.a.b.world = "hello" print(testtable.a.b.hello) print(testtable.a.b.world) print(isautotable(testtable.a)) print(isautotable(testtable.a.b)) table.insert(testtable.a.b, "hello world 1") table.insert(testtable.a.b, "hello world 2") for k,v in pairs(testtable.a.b) do print(k,v) end print(testtable.a, isautotable(testtable.a)) --> table, true print(testtable.a.b, isautotable(testtable.a.b)) --> table, false print(testtable.a.b.c) --> nil print(testtable.a.b.c.d) --> error