← Volver al listado de tecnologías

Capítulo 3: Diccionarios y Conjuntos

Por: Artiko
luatablesdictionarieshash-mapssets

Capítulo 3: Diccionarios y Conjuntos

“La simplicidad es el resultado final de la sofisticación.” — Leonardo da Vinci

En el capítulo anterior vimos tablas como arrays (secuencias ordenadas). Ahora las veremos como diccionarios (hash maps) y conjuntos (sets).

La belleza de Lua es que no necesitas aprender una nueva sintaxis o estructura. Es la misma tabla, solo la usas de forma diferente.

Tablas como Diccionarios

Un diccionario (o hash map, o tabla asociativa) mapea claves a valores:

-- >>> person = {
-- >>>     name = 'Alice',
-- >>>     age = 30,
-- >>>     city = 'Madrid'
-- >>> }
-- >>> print(person.name)
-- Alice
-- >>> print(person['age'])
-- 30

Dos Sintaxis Equivalentes

Hay dos formas de acceder a valores:

-- Dot notation (azúcar sintáctica)
-- >>> print(person.name)
-- Alice

-- Bracket notation (forma completa)
-- >>> print(person['name'])
-- Alice

Ambas son exactamente lo mismo. person.name es solo azúcar sintáctica para person['name'].

IMPORTANTE: Cuándo usar cada una

  • Dot notation: Cuando la clave es un identificador válido (letters, números, underscore, no empieza con número)
  • Bracket notation: Cuando la clave tiene espacios, caracteres especiales, o es una variable
-- ✅ Dot notation funciona
person.name = 'Bob'

-- ❌ Dot notation NO funciona (espacio en key)
-- person.full name = 'Bob Smith'  -- Error de sintaxis

-- ✅ Bracket notation funciona
person['full name'] = 'Bob Smith'

-- ✅ Bracket notation con variable
local key = 'age'
print(person[key])  -- 30

Claves Válidas

En Lua, casi cualquier valor puede ser una clave, excepto nil y NaN (Not a Number):

-- >>> weird = {}
-- >>> weird[42] = 'número'
-- >>> weird['foo'] = 'string'
-- >>> weird[true] = 'boolean'
-- >>> weird[{}] = 'tabla'  -- ¡Sí, una tabla como clave!
-- >>> print(weird[42])
-- número
-- >>> print(weird['foo'])
-- string

Pero hay dos valores que NO pueden ser claves:

-- ❌ nil como clave
-- >>> t = {}
-- >>> t[nil] = 'valor'
-- Error: table index is nil

-- ❌ NaN como clave
-- >>> t[0/0] = 'valor'
-- Error: table index is NaN

DEEP DIVE: Por qué nil no puede ser clave

En Lua, nil significa “ausencia de valor”. Cuando haces t[key] = nil, estás eliminando esa clave de la tabla:

-- >>> t = {foo = 'bar'}
-- >>> print(t.foo)
-- bar
-- >>> t.foo = nil  -- Eliminar la clave 'foo'
-- >>> print(t.foo)
-- nil

Si permitieras nil como clave, habría ambigüedad: ¿t[nil] significa “la clave nil” o “ausencia de clave”?

Agregar y Eliminar Claves

Agregar es simple: solo asigna:

-- >>> person = {name = 'Alice'}
-- >>> person.age = 30
-- >>> person['city'] = 'Madrid'
-- >>> print(person.age, person.city)
-- 30    Madrid

Eliminar: asigna nil:

-- >>> person.age = nil
-- >>> print(person.age)
-- nil

Verificar si una Clave Existe

Hay dos formas:

-- Método 1: Comparar con nil
-- >>> if person.age ~= nil then
-- >>>     print('age existe')
-- >>> end

-- Método 2: Usar next() (más robusto)
-- >>> if next(person, 'age') ~= nil then
-- >>>     print('age existe')
-- >>> end

Pero cuidado: si el valor almacenado es nil, ambas formas dirán que la clave “no existe”, porque en Lua asignar nil elimina la clave.

Iterar sobre Diccionarios

Usa pairs (no ipairs, que es solo para secuencias):

-- >>> person = {name = 'Alice', age = 30, city = 'Madrid'}
-- >>> for key, value in pairs(person) do
-- >>>     print(key, value)
-- >>> end
-- name    Alice
-- age     30
-- city    Madrid

ADVERTENCIA: Orden Indefinido

El orden de iteración con pairs es indefinido. No confíes en un orden específico:

-- Este código puede imprimir en cualquier orden:
for k, v in pairs(person) do
    print(k)
end
-- Podría imprimir: age, name, city
-- O podría ser: name, city, age
-- No hay garantías

Si necesitas orden, tienes dos opciones:

Opción 1: Ordenar las Claves

local function sorted_pairs(t)
    local keys = {}
    for k in pairs(t) do
        table.insert(keys, k)
    end
    table.sort(keys)

    local i = 0
    return function()
        i = i + 1
        local key = keys[i]
        if key then
            return key, t[key]
        end
    end
end

-- >>> for key, value in sorted_pairs(person) do
-- >>>     print(key, value)
-- >>> end
-- age     30
-- city    Madrid
-- name    Alice

Opción 2: Mantener un Array de Claves

-- >>> person = {
-- >>>     _keys = {'name', 'age', 'city'},
-- >>>     name = 'Alice',
-- >>>     age = 30,
-- >>>     city = 'Madrid'
-- >>> }
-- >>> for _, key in ipairs(person._keys) do
-- >>>     print(key, person[key])
-- >>> end
-- name    Alice
-- age     30
-- city    Madrid

Contar Elementos en un Diccionario

El operador # NO funciona con diccionarios (solo con secuencias):

-- >>> person = {name = 'Alice', age = 30, city = 'Madrid'}
-- >>> print(#person)
-- 0  -- ¡Incorrecto!

Necesitas contar manualmente:

local function table_size(t)
    local count = 0
    for _ in pairs(t) do
        count = count + 1
    end
    return count
end

-- >>> print(table_size(person))
-- 3

O mantener un contador:

local SmartDict = {}
SmartDict.__index = SmartDict

function SmartDict.new()
    return setmetatable({_count = 0, _data = {}}, SmartDict)
end

function SmartDict:set(key, value)
    if self._data[key] == nil and value ~= nil then
        self._count = self._count + 1
    elseif self._data[key] ~= nil and value == nil then
        self._count = self._count - 1
    end
    self._data[key] = value
end

function SmartDict:get(key)
    return self._data[key]
end

function SmartDict:size()
    return self._count
end

Tablas como Conjuntos (Sets)

Un conjunto es una colección de valores únicos, sin duplicados. Lua no tiene un tipo set nativo, pero puedes implementarlo eficientemente con una tabla:

El Truco: Keys = Elementos, Values = true

En lugar de almacenar elementos en un array:

-- ❌ Array (ineficiente para búsqueda)
local tags = {'lua', 'programming', 'tutorial'}

-- Buscar si 'lua' existe requiere iterar todo el array: O(n)
local function contains(arr, value)
    for _, v in ipairs(arr) do
        if v == value then return true end
    end
    return false
end

Almacénalos como claves:

-- ✅ Set (búsqueda eficiente)
local tags = {
    lua = true,
    programming = true,
    tutorial = true
}

-- Buscar si 'lua' existe es O(1):
-- >>> print(tags.lua)
-- true
-- >>> print(tags.python)
-- nil

Implementar un Set

local Set = {}
Set.__index = Set

function Set.new(elements)
    local self = setmetatable({_data = {}}, Set)
    if elements then
        for _, elem in ipairs(elements) do
            self:add(elem)
        end
    end
    return self
end

function Set:add(elem)
    self._data[elem] = true
end

function Set:remove(elem)
    self._data[elem] = nil
end

function Set:contains(elem)
    return self._data[elem] == true
end

function Set:size()
    local count = 0
    for _ in pairs(self._data) do
        count = count + 1
    end
    return count
end

function Set:to_array()
    local arr = {}
    for elem in pairs(self._data) do
        table.insert(arr, elem)
    end
    return arr
end

function Set:__tostring()
    local elements = self:to_array()
    table.sort(elements, function(a, b)
        return tostring(a) < tostring(b)
    end)
    return 'Set{' .. table.concat(elements, ', ') .. '}'
end

Uso:

-- >>> s = Set.new({'lua', 'python', 'javascript'})
-- >>> print(s)
-- Set{javascript, lua, python}
-- >>> s:add('ruby')
-- >>> print(s:contains('lua'))
-- true
-- >>> print(s:contains('go'))
-- false
-- >>> s:remove('python')
-- >>> print(s:size())
-- 3

Operaciones de Conjuntos

Implementemos unión, intersección, y diferencia:

function Set:union(other)
    local result = Set.new()
    for elem in pairs(self._data) do
        result:add(elem)
    end
    for elem in pairs(other._data) do
        result:add(elem)
    end
    return result
end

function Set:intersection(other)
    local result = Set.new()
    for elem in pairs(self._data) do
        if other:contains(elem) then
            result:add(elem)
        end
    end
    return result
end

function Set:difference(other)
    local result = Set.new()
    for elem in pairs(self._data) do
        if not other:contains(elem) then
            result:add(elem)
        end
    end
    return result
end

function Set:is_subset(other)
    for elem in pairs(self._data) do
        if not other:contains(elem) then
            return false
        end
    end
    return true
end

-- Sobrecarga de operadores
function Set:__add(other)  -- Unión: s1 + s2
    return self:union(other)
end

function Set:__mul(other)  -- Intersección: s1 * s2
    return self:intersection(other)
end

function Set:__sub(other)  -- Diferencia: s1 - s2
    return self:difference(other)
end

function Set:__le(other)  -- Subset: s1 <= s2
    return self:is_subset(other)
end

Uso:

-- >>> s1 = Set.new({'lua', 'python', 'ruby'})
-- >>> s2 = Set.new({'python', 'javascript', 'go'})

-- >>> union = s1 + s2
-- >>> print(union)
-- Set{go, javascript, lua, python, ruby}

-- >>> intersection = s1 * s2
-- >>> print(intersection)
-- Set{python}

-- >>> difference = s1 - s2
-- >>> print(difference)
-- Set{lua, ruby}

Caso Práctico: Sistema de Tags

Implementemos un sistema de tags para objetos de un juego:

local TagSystem = {}
TagSystem.__index = TagSystem

function TagSystem.new()
    local self = setmetatable({}, TagSystem)
    self.objects = {}  -- object_id -> Set de tags
    self.tags = {}     -- tag -> Set de object_ids
    return self
end

function TagSystem:add_tag(object_id, tag)
    -- Asegurar que el objeto existe
    if not self.objects[object_id] then
        self.objects[object_id] = Set.new()
    end

    -- Asegurar que el tag existe
    if not self.tags[tag] then
        self.tags[tag] = Set.new()
    end

    -- Agregar relaciones
    self.objects[object_id]:add(tag)
    self.tags[tag]:add(object_id)
end

function TagSystem:remove_tag(object_id, tag)
    if self.objects[object_id] then
        self.objects[object_id]:remove(tag)
    end

    if self.tags[tag] then
        self.tags[tag]:remove(object_id)
    end
end

function TagSystem:has_tag(object_id, tag)
    return self.objects[object_id] and self.objects[object_id]:contains(tag)
end

function TagSystem:get_objects_with_tag(tag)
    if self.tags[tag] then
        return self.tags[tag]:to_array()
    end
    return {}
end

function TagSystem:get_tags_for_object(object_id)
    if self.objects[object_id] then
        return self.objects[object_id]:to_array()
    end
    return {}
end

function TagSystem:get_objects_with_all_tags(...)
    local required_tags = {...}
    if #required_tags == 0 then return {} end

    -- Empezar con objetos que tienen el primer tag
    local result_set = nil
    for _, tag in ipairs(required_tags) do
        if not self.tags[tag] then
            return {}  -- Si algún tag no existe, retornar vacío
        end

        if not result_set then
            result_set = self.tags[tag]
        else
            result_set = result_set:intersection(self.tags[tag])
        end
    end

    return result_set and result_set:to_array() or {}
end

Uso:

-- >>> ts = TagSystem.new()
-- >>> ts:add_tag('player1', 'hero')
-- >>> ts:add_tag('player1', 'alive')
-- >>> ts:add_tag('enemy1', 'monster')
-- >>> ts:add_tag('enemy1', 'alive')
-- >>> ts:add_tag('enemy2', 'monster')

-- >>> print(table.concat(ts:get_tags_for_object('player1'), ', '))
-- alive, hero

-- >>> print(table.concat(ts:get_objects_with_tag('alive'), ', '))
-- enemy1, player1

-- >>> print(table.concat(ts:get_objects_with_all_tags('monster', 'alive'), ', '))
-- enemy1

DEEP DIVE: Hash Tables Internamente

Cuando usas claves no-numéricas o índices no-consecutivos, Lua usa una hash table.

Función de Hash

Lua tiene una función de hash interna para cada tipo:

Colisiones

Cuando dos claves tienen el mismo hash, Lua usa chaining (listas enlazadas) para resolver la colisión.

Hash Table:
[0] -> nil
[1] -> ('foo', 10) -> ('bar', 20)  -- Colisión
[2] -> ('baz', 30)
[3] -> nil

Performance

NOTA: Lua rehashea automáticamente

Cuando la tabla crece, Lua rehashea automáticamente para mantener buen rendimiento. No necesitas preocuparte por esto.

Cuándo Usar Qué

NecesidadUsa
Secuencia ordenadaArray (índices 1..N)
Mapeo key-valueDiccionario
Búsqueda rápida (existe/no existe)Set
Elementos únicos sin ordenSet
Elementos únicos con ordenArray + verificación manual

Resumen del Capítulo

Las tablas de Lua como diccionarios y sets:

Próximo: Capítulo 4: Strings - Inmutables y Compartidos


Ejercicios

  1. Invertir un Diccionario: Escribe una función que intercambie keys y values.
function invert(dict)
    -- Tu código aquí
end

-- >>> person = {name = 'Alice', age = 30}
-- >>> inverted = invert(person)
-- >>> print(inverted.Alice)
-- name
  1. Merge de Diccionarios: Combina dos diccionarios.
function merge(dict1, dict2)
    -- Tu código aquí
end

-- >>> a = {x = 1, y = 2}
-- >>> b = {y = 3, z = 4}
-- >>> c = merge(a, b)
-- >>> print(c.x, c.y, c.z)
-- 1    3    4
  1. Conjunto de Potencias: Dado un set, genera todos sus subconjuntos.
function power_set(s)
    -- Tu código aquí
end

-- >>> s = Set.new({1, 2, 3})
-- >>> ps = power_set(s)
-- {} {1} {2} {3} {1,2} {1,3} {2,3} {1,2,3}