Tablas en Lua: la única estructura de datos que lo hace todo

Si tuvieras que describir Lua en una sola frase técnica, podría ser esta: un lenguaje donde todo lo que no es un valor primitivo es una tabla. No hay arrays separados, ni clases, ni structs, ni enumeraciones. Solo hay tablas, y con ellas se construye todo lo demás. Esa apuesta radical por la simplicidad es, paradójicamente, lo que hace a Lua tan flexible.

Qué es una tabla en Lua

Una tabla (table) es una colección asociativa heterogénea: puede tener claves de cualquier tipo (excepto nil) apuntando a valores de cualquier tipo (excepto nil). Internamente, la VM de Lua divide cada tabla en dos partes:

  • Parte array: claves enteras consecutivas empezando en 1. Se almacenan en un array contiguo de C para máxima velocidad de acceso.
  • Parte hash: el resto de pares clave-valor, en una tabla hash abierta.

Esta división es transparente al programador pero tiene implicaciones de rendimiento: acceder a t[1], t[2]... es tan rápido como en un array de C, mientras que acceder a t["nombre"] usa la tabla hash.

Creación y acceso básico

-- Tabla vacía
local t = {}

-- Tabla con parte array (índices 1..3)
local frutas = {"manzana", "pera", "naranja"}
print(frutas[1])   -- manzana
print(#frutas)     -- 3 (longitud del segmento array)

-- Tabla con parte hash
local persona = {
    nombre = "Ana",
    edad   = 30,
    activa = true
}
print(persona.nombre)        -- Ana (sintaxis de punto)
print(persona["edad"])       -- 30 (sintaxis de corchetes, equivalente)

-- Mezcla de array y hash
local mixta = {10, 20, 30, color = "rojo", peso = 1.5}
print(mixta[2])       -- 20
print(mixta.color)    -- rojo
print(#mixta)         -- 3 (solo cuenta la parte array)

Modificar tablas en tiempo de ejecución

Las tablas son objetos mutables que se pasan por referencia. Asignar una tabla a otra variable no la copia: ambas apuntan al mismo objeto.

local a = {1, 2, 3}
local b = a          -- b y a apuntan a la MISMA tabla
b[4] = 4
print(#a)            -- 4, porque a y b son la misma tabla

-- Para copiar hay que iterar:
local function copiarTabla(origen)
    local copia = {}
    for k, v in pairs(origen) do
        copia[k] = v
    end
    return copia
end

-- Eliminar una clave: asignar nil
a[2] = nil
-- OJO: esto crea un "agujero" en el array; #a puede ser impredecible

Iteración: ipairs vs pairs

Lua ofrece dos iteradores estándar para tablas, y elegir el correcto importa:

  • ipairs: recorre la parte array en orden ascendente (1, 2, 3…) y se detiene en el primer nil. Ideal para arrays sin agujeros.
  • pairs: recorre todos los pares clave-valor (array + hash) en orden arbitrario. Imprescindible para tablas como diccionarios.
local datos = {
    "primero",   -- [1]
    "segundo",   -- [2]
    "tercero",   -- [3]
    extra = "campo hash"
}

-- ipairs: solo el array, en orden
for i, v in ipairs(datos) do
    print(i, v)
    -- 1  primero
    -- 2  segundo
    -- 3  tercero
    -- NO imprime "extra"
end

-- pairs: todo, en orden indefinido
for k, v in pairs(datos) do
    print(k, v)
    -- 1  primero
    -- 2  segundo
    -- 3  tercero
    -- extra  campo hash  (en algún momento, orden no garantizado)
end

Tablas como módulos

El sistema de módulos de Lua se basa en tablas. La función require carga un fichero y devuelve lo que éste retorne, que por convención es una tabla con las funciones públicas:

-- fichero: geometria.lua
local M = {}

function M.areaCirculo(r)
    return math.pi * r * r
end

function M.perimetroCirculo(r)
    return 2 * math.pi * r
end

return M

-- En otro fichero:
local geo = require("geometria")
print(geo.areaCirculo(5))      -- 78.539...
print(geo.perimetroCirculo(5)) -- 31.415...

Tablas como arrays dinámicos

La biblioteca estándar table ofrece las operaciones habituales sobre la parte array:

local pila = {}

-- Insertar al final (equivalente a push)
table.insert(pila, "a")
table.insert(pila, "b")
table.insert(pila, "c")

-- Insertar en posición concreta
table.insert(pila, 2, "X")   -- {a, X, b, c}

-- Eliminar (y devolver) el último elemento
local ultimo = table.remove(pila)   -- "c"

-- Eliminar en posición concreta
local segundo = table.remove(pila, 2)  -- "X"

-- Concatenar todos los elementos string
local lista = {"rojo", "verde", "azul"}
print(table.concat(lista, ", "))  -- rojo, verde, azul

-- Ordenar in-place
local numeros = {5, 3, 8, 1, 9, 2}
table.sort(numeros)
-- {1, 2, 3, 5, 8, 9}

-- Ordenar con comparador personalizado
table.sort(numeros, function(a, b) return a > b end)
-- {9, 8, 5, 3, 2, 1}

Tablas anidadas y estructuras complejas

Como las tablas pueden contener otras tablas, modelar estructuras jerárquicas es natural. Esto conecta directamente con el sistema de metatables para orientación a objetos que veremos más adelante en esta serie:

-- Árbol binario de búsqueda
local function nuevoNodo(valor)
    return {valor = valor, izq = nil, der = nil}
end

local function insertar(raiz, valor)
    if raiz == nil then
        return nuevoNodo(valor)
    elseif valor < raiz.valor then
        raiz.izq = insertar(raiz.izq, valor)
    else
        raiz.der = insertar(raiz.der, valor)
    end
    return raiz
end

local function inorden(nodo, resultado)
    resultado = resultado or {}
    if nodo then
        inorden(nodo.izq, resultado)
        table.insert(resultado, nodo.valor)
        inorden(nodo.der, resultado)
    end
    return resultado
end

local raiz = nil
for _, v in ipairs({5, 3, 7, 1, 4, 6, 8}) do
    raiz = insertar(raiz, v)
end
print(table.concat(inorden(raiz), ", "))
-- 1, 3, 4, 5, 6, 7, 8

El operador longitud y sus sorpresas

El operador # devuelve la longitud de la parte array de una tabla, pero con una condición: el resultado es indefinido si la tabla tiene agujeros (posiciones nil entre valores no nulos). Para arrays sin agujeros funciona perfectamente y en tiempo O(log n) usando búsqueda binaria sobre la parte array interna.

Si necesitas contar todos los elementos de una tabla (incluida la parte hash), debes iterarla manualmente:

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

Entender la tabla de Lua —su dualidad array/hash, la semántica de referencia y los dos iteradores— es la base sobre la que se construye todo lo demás en el lenguaje. Las corrutinas, los módulos y la orientación a objetos son, al fin y al cabo, tablas con comportamientos especiales añadidos.

Imagen: Pexels / Digital Buggu

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP