Lua: Как посмотреть в таблице, где ключи - это таблицы (или объекты)

Я хочу хранить таблицу lua, где ключами являются другие таблицы lua. Я знаю, что это возможно, но я хочу иметь возможность искать в таблице, используя копии этих таблиц. В частности, я хочу иметь возможность сделать:

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

и тогда я хочу иметь возможность посмотреть:

t[key2]

и получить 4.

Я знаю что могу превратить key в строку и положить его в таблицу t, Я также думал о написании пользовательской хеш-функции или выполнении этого путем вложения таблиц. Есть ли лучший способ для меня, чтобы получить этот тип функциональности? Какие еще варианты у меня есть?

7 ответов

Решение

В Lua две таблицы, созданные отдельно, считаются "разными". Но если вы создадите таблицу один раз, вы можете назначить ее любым переменным, которые захотите, и когда вы сравните их, Lua скажет вам, что они равны. Другими словами:

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

Итак, это простой, чистый способ делать то, что вы хотите. хранить key где-то, так что вы можете получить 4 назад, используя его. Это тоже очень быстро.

Если ты действительно не хочешь этого делать... ну, есть способ. Но это неэффективно и некрасиво.

Первая часть - создание функции, которая сравнивает две отдельные таблицы. Он должен возвращать true, если две таблицы "эквивалентны", и false, если они не являются. Давайте назовем это эквивалентным. Это должно работать так:

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

Функция должна быть рекурсивной, чтобы обрабатывать таблицы, которые содержат сами таблицы. Это также не должно быть одурачено, если одна из таблиц "содержит" другую, но имеет больше элементов. Я вышел с этой реализацией; вероятно, есть лучшие из них там.

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

Я не собираюсь объяснять эту функцию здесь. Надеюсь, достаточно ясно, что он делает.

Другая часть головоломки состоит в создании t использовать equivalent функция при сравнении ключей. Это можно сделать с помощью аккуратных метатабируемых манипуляций и дополнительной таблицы "хранения".

Мы в основном трансформируем t в самозванца. Когда наш код говорит ему сохранить значение под ключом, он не сохраняет его сам по себе; вместо этого он дает его к дополнительной таблице (мы будем называть это store). Когда код спрашивает t для значения, оно ищет его в store, но используя equivalent Функция, чтобы получить это.

Это код:

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
})

Пример использования:

t[{a = 1}] = 4

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

Ответ Кикито хорош, но имеет некоторые недостатки:

  • Если вы выполняете t[{a=1}] = true два раза, store будет содержать две таблицы (утечка памяти за время существования хеш-таблицы)
  • Изменение значения после того, как вы уже сохранили его, не работает, и вы не можете его удалить. Попытка изменить его приведет к тому, что поиск потенциально вернет любое значение, которое вы присвоили этой клавише в прошлом.
  • Производительность доступа - O(n) (n - количество сохраненных записей и предполагается, что извлечение значения lua из таблицы - O(1)); в сочетании с первым пунктом производительность этой хеш-таблицы будет ухудшаться при использовании

(Также обратите внимание, что "эквивалентная" функция kikito вызовет бесконечный цикл, если в любой таблице есть циклическая ссылка.)

Если вам никогда не нужно менять / удалять какую-либо информацию в таблице, то ответа kikito будет достаточно в том виде, в каком он есть. В противном случае, метатабельность должна быть изменена так, чтобы __newindex убедился, что таблица еще не существует:

t = setmetatable({}, {
    __newindex = function(tbl, key, value)
        for k,v in pairs(store) do
            if equivalent(k,key) then
                tbl[k] = value
                return
            end
        end
        store[key] = value
    end,
    __index = function(tbl, key)
        for k,v in pairs(store) do
            if equivalent(k, key) then return v end
        end
    end
})

Как вы предложили, совершенно другой вариант - написать собственную функцию хеширования. Вот HashTable, который может использовать это:

local function HashTable(Hash, Equals)
    --Hash is an optional function that takes in any key and returns a key that lua can use (string or number). If you return false/nil, it will be assumed that you don't know how to hash that value.
    --    If Hash is not provided, table-keys should have a GetHash function or a .Hash field
    --Equals is an optional function that takes two keys and specify whether they are equal or not. This will be used when the same hash is returned from two keys.
    --    If Equals is not provided, items should have a Equals function; items are in this case assumed to not be equal if they are different types.
    local items = {} --Dict<hash, Dict<key, value>>
    local function GetHash(item)
        return Hash and Hash(item) or type(item) == "table" and (item.GetHash and item:GetHash() or item.Hash) or item
    end
    local function GetEquals(item1, item2)
        if Equals then return Equals(item1, item2) end
        local t1, t2 = type(item1), type(item2)
        if t1 ~= t2 then return false end
        if t1 == "table" and item1.Equals then
            return item1:Equals(item2)
        elseif t2 == "table" and item2.Equals then
            return item2:Equals(item1)
        end
        return false
    end
    return setmetatable({}, {
        __newindex = function(_, key, value)
            local hash = GetHash(key)
            local dict = items[hash]
            if not dict then
                if value ~= nil then --Only generate a table if it will be non-empty after assignment
                    items[hash] = {[key] = value}
                end
                return
            end
            for k, v in pairs(dict) do
                if GetEquals(key, k) then --Found the entry; update it
                    dict[k] = value
                    if value == nil then --check to see if dict is empty
                        if next(dict) == nil then
                            items[hash] = nil
                        end
                    end
                    return
                end
            end
            --This is a unique entry
            dict[key] = value
        end,
        __index = function(_, key)
            local hash = GetHash(key)
            local dict = items[hash]
            if not dict then return nil end
            for k, v in pairs(dict) do
                if GetEquals(key, k) then
                    return v
                end
            end
        end
    })
end

Пример использования:

local h = HashTable(
    function(t) return t.a or 0 end, --Hash
    function(t1, t2) return t1.a == t2.a end) --Equals
h[{a=1}] = 1
print(h[{a=1}]) -- 1
h[{a=1}] = 2
print(h[{a=1}]) -- 2
print(h[{a=1,b=2}]) -- 2 because Hash/Equals only look at 'a'

Естественно, вы захотите получить лучшие функции Hash/Equals.

Пока хэши ваших ключей редко сталкиваются, производительность этого класса должна быть O (1).

(Примечание: я бы поместил верхнюю половину этого ответа в качестве комментария к kikito, но у меня нет репутации, чтобы сделать это.)

Это невозможно в Луа. Если вы используете таблицы в качестве ключей, ключ - это конкретный "экземпляр" таблицы. Даже если вы создаете другую таблицу с одинаковым содержимым, экземпляр отличается, поэтому это другой ключ.

Если вы хотите сделать что-то подобное, вы можете создать своего рода хеш-функцию, которая обходит таблицу, чтобы служить ключом (возможно, даже рекурсивно, если необходимо), и создать строковое представление содержимого таблицы. Он не должен быть удобочитаемым, если он различен для разного содержимого и одинаков для таблиц с одинаковым содержимым. Помимо использования pairs() чтобы пройти таблицу, вам также нужно вставить ключи в таблицу и отсортировать их, используя table.sort(), так как pairs() возвращает их в произвольном порядке, и вы хотите одну и ту же строку для "равных" таблиц.

После того, как вы сконструировали такую ​​строку, вы можете использовать ее в качестве ключа:

function hash(t) ... end
t = {}
key1 = { a = "a", b = "b" }
t[hash(key1)] = 4
key2 = { a = "a", b = "b" }
print(t[hash(key2)]) -- should print "4" if the hash function works correctly

На мой взгляд, все это слишком сложно для простой задачи индексации, и вы можете переосмыслить свое желание индексировать, используя копии таблиц. Зачем вам такая функциональность?

Обновить

Если вам нужно работать только с фразами, я думаю, что объединить их проще, чем создавать такую ​​общую хэш-функцию. Если вам это нужно для последовательности фраз, вам на самом деле не нужно будет перебирать таблицы и сортировать ключи, просто собирайте основную информацию из каждой фразы. Вам по-прежнему нужно использовать вспомогательную функцию, которая может создать подходящий для вас ключ:

function pkey(...)
    local n, args = select('#', ...), { ... }
    for i=1,n do args[i] = args[i].value end -- extract your info here
    return table.concat(args, ' ') -- space or other separator, such as ':'          
end
tab[pkey(phrase1, phrase2, phrase3)] = "value"

Я не очень много знаю о языковой обработке и о цели, которую вы хотите достичь с помощью своей программы, но как насчет сбора токенов, например, так: используйте структуру вложенных таблиц, в которой таблицы индексов хранят только таблицы, проиндексированные токеном первой фразы, тогда каждая подтаблица содержит значение, проиндексированное токеном второй фразы... и т. д.... пока вы не достигнете окончательного токена фразы, будет индексировать числовое значение, соответствующее вхождению фразы.

Может быть, это будет более понятно с примером, если у вас есть две следующие фразы:

  • Мне нравится банан.
  • Мне нравится горячая цыпочка.

Ваш индекс будет иметь следующую структуру:

index["I"] = {
    ["like"] = {
        ["banana"] = 1,
        ["hot"] = {
            ["chick"] = 1
        }
    }    
}

Таким образом, вы можете подсчитывать частоты с помощью одного шага обхода и подсчитывать вхождения в то же время, когда вы индексируете, но, как я уже говорил, это зависит от вашей цели, и это будет означать повторное разбиение вашей фразы так, чтобы найти случаи через ваш индекс.

Я думаю, что самый простой способ сделать что-то подобное - это иметьtableKeyфабрика, которая возвращает таблицы только для чтения для введенной таблицы.

Вы хотели бы назначить__key(или подобное) значение для использования в качестве ключей. Что-то вроде:

      _cachedTableKeys = {}
function tableKey (t)
  if(not t.__key) t.__key = calculateKey(t)
  local existing = _cachedTableKeys[t.__key]
  if(existing) return existing
  existing = readOnly(t) -- https://www.lua.org/pil/13.4.5.html
  _cachedTableKeys[t.__key] = existing
  return existing 
end

Это гарантирует, что таблица, используемая в качестве ключа, доступна только для чтения и что ее идентификатор всегда будет совпадать, если он равен.

Обратите вниманиеcalculateKeyфункция будет рекурсивно выполнять хеш-функцию для ключей таблицы. Вы хотите использовать достаточно большой хеш (например, 16 байт или около того), чтобы коллизия была практически невозможна. Вы также можете реализовать какой-то алгоритм коллизий при поиске таблицы по ключу.

Я не уверен, что вы можете сделать это. Вы можете определить равенство для таблиц, используя метатаблицу, но нет способа определить хеш-функцию, и я сомневаюсь, что определение равенства само по себе сделает то, что вам нужно. Очевидно, вы можете определить равенство, а затем перебрать таблицу, используя pairs() и сравнивая ключи самостоятельно, но это превратит то, что должно быть O(1) поиск в O(n),

Ответ kikito имеет начало решения, но, как отмечается в ответе chess123mate , он предназначен только для записи (среди других недостатков). Это решение не устраняет их всех, но это только начало. (Это тоже очень и очень медленно.)

      local function equivalent(stack)
    local a, b = stack.a, stack.b

    if type(a) ~= 'table' then return a == b end
    if a == b then return true end

    local top = stack.next
    while top ~= nil do
        if stack.a == a and stack.b == b then return true end
        top = stack.next
    end

    local counta, countb = 0, 0

    for k,va in pairs(a) do
        if not equivalent({a = va, b = b[k], next = stack}) then return false end
        counta = counta + 1
    end

    for _,_ in pairs(b) do countb = countb + 1 end

    return counta == countb
end

t = setmetatable({}, {
    __newindex = function(tbl, key, value)
        for k, _ in pairs(tbl) do
            if equivalent({a = k, b = key}) then rawset(tbl, k, value); return end
        end
        rawset(tbl, key, value)
    end,
    __index = function(tbl, key)
        for k, v in pairs(tbl) do
            if equivalent({a = k, b = key}) then return v end
        end
    end
})

Ссылка Lua Playground (или ссылка GitHub Gist для копирования и вставки на игровую площадку, если ваш браузер меня ненавидит за это последнее).

Другие вопросы по тегам