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 primernil. 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
