Automagic Tables

lua-users home
wiki

Building on some of the techniques illustrated in FuncTables, here are some implementations of Perl-style[1] automagic tables (also called autovivication). An automagic table creates subtables on demand, as it were; i.e.:

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


A comment made by ThomasWrensch seemed relevant, I quote it without permission so I hope he doesn't mind:

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:

1. Put a key in the table with the information
2. Use the table as a key in another (possibly weak) table.
3. Associate it with the metatable.

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.

-- ThomasWrensch

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

RecentChanges · preferences
edit · history
Last edited March 17, 2016 1:24 pm GMT (diff)