Capítulo 3: Diccionarios y Conjuntos
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,
nilsignifica “ausencia de valor”. Cuando hacest[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) -- nilSi permitieras
nilcomo 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
pairses 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:
- Strings: Hash basado en contenido (strings iguales → mismo hash)
- Numbers: Conversión directa a int
- Tables, functions: Hash basado en la dirección de memoria
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
- Búsqueda promedio: O(1)
- Peor caso (todas las claves colisionan): O(n)
- Inserción/eliminación promedio: O(1)
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é
| Necesidad | Usa |
|---|---|
| Secuencia ordenada | Array (índices 1..N) |
| Mapeo key-value | Diccionario |
| Búsqueda rápida (existe/no existe) | Set |
| Elementos únicos sin orden | Set |
| Elementos únicos con orden | Array + verificación manual |
Resumen del Capítulo
Las tablas de Lua como diccionarios y sets:
- Cualquier valor puede ser clave (excepto
nilyNaN) - Dot notation vs bracket notation: son equivalentes
pairspara iterar (orden indefinido)#no funciona con diccionarios (solo secuencias)- Sets eficientes:
{elem = true}en lugar de arrays - Operaciones de conjuntos: unión, intersección, diferencia
Próximo: Capítulo 4: Strings - Inmutables y Compartidos
Ejercicios
- 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
- 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
- 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}