Search code examples
luahashtablelua-table

Lua: How to look up in a table where the keys are tables (or objects)


I want to store a lua table where the keys are other lua tables. I know that this is possible BUT I want to be able to do look ups in the table using copies of those tables. Specifically, I want to be able to do:

t = {}
key = { a = "a" }
t[key] = 4
key2 = { a = "a" }

and then I want to be able to look up:

t[key2]

and get 4.

I know that I can turn key into a string and put it into table t. I've also thought about writing a custom hash function or doing this by nesting tables. Is there a best way for me to get this type of functionality? What other options do I have?


Solution

  • In Lua, two tables created separately are considered "different". But if you create a table once, you can assign it to any variables you want, and when you compare them, Lua will tell you that they are equal. In other words:

    t = {}
    key = { a = "a" }
    t[key] = 4
    key2 = key
    ...
    t[key2] -- returns 4
    

    So, that's the simple, clean way of doing what you want. Store key somewhere, so you can retrieve the 4 back by using it. This is also very fast.

    If you really don't want to do that ... well, there is a way. But it is kindof inefficient and ugly.

    The first part is making a function that compares two separate tables. It should return true if two tables are "equivalent", and false if they are not. Let's call it equivalent. It should work like this:

    equivalent({a=1},{a=1})          -- true
    equivalent({a=1,b=2}, {a=1})     -- false
    equivalent({a={b=1}}, {a={b=2}}) -- false
    

    The function must be recursive, to handle tables that contain tables themselves. It also must not be fooled if one of the tables "contains" the other, but has more elements. I came out with this implementation; probably there are better ones out there.

    local function equivalent(a,b)
      if type(a) ~= 'table' then return a == b end
    
      local counta, countb = 0, 0
    
      for k,va in pairs(a) do
        if not equivalent(va, b[k]) then return false end
        counta = counta + 1
      end
    
      for _,_ in pairs(b) do countb = countb + 1 end
    
      return counta == countb
    end
    

    I'm not going to explain that function here. I hope it is clear enough what it does.

    The other part of the puzzle consist on making t use the equivalent function when comparing keys. This can be done with careful metatable manipulation, and an extra "storage" table.

    We basically transform t into an impostor. When our code tells it to store a value under a key, it doesn't save it in itself; instead it gives it to the extra table (we'll call that store). When the code asks t for a value, it searches for it in store, but using the equivalent function to get it.

    This is the code:

    local function equivalent(a,b)
    ... -- same code as before
    end
    
    local store = {} -- this is the table that stores the values
    
    t = setmetatable({}, {
      __newindex = store,
      __index = function(tbl, key)
        for k,v in pairs(store) do
          if equivalent(k,key) then return v end
        end
      end
    })
    

    Usage example:

    t[{a = 1}] = 4
    
    print(t[{a = 1}]) -- 4
    print(t[{a = 1, b = 2}]) -- nil