← Volver al listado de tecnologías

Capítulo 2: Tablas - Arrays y Secuencias

Por: Artiko
luatablesarrayssequencesdata-structures

Capítulo 2: Tablas - Arrays y Secuencias

“Haz una cosa y hazla bien.” — Filosofía Unix

En la mayoría de lenguajes de programación, tienes múltiples estructuras de datos: arrays, listas enlazadas, hash maps, sets, etc. En Lua, tienes una sola estructura de datos: la tabla.

Antes de que pienses “eso suena limitante”, considera esto: las tablas de Lua son tan versátiles y eficientes que pueden simular todas esas estructuras. Y como solo hay una estructura, dominarla te hace proficient en Lua.

En este capítulo, cubriremos las tablas como arrays (secuencias ordenadas de valores). En el siguiente, las veremos como diccionarios (hash maps).

Lo Básico: Crear y Usar Arrays

En Lua, crear un array es tan simple como esto:

-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> print(fruits[1])
-- manzana
-- >>> print(fruits[2])
-- banana
-- >>> print(fruits[3])
-- cereza

Índices Desde 1 (No Desde 0)

La característica más comentada (y a veces odiada) de Lua: los índices empiezan en 1, no en 0.

-- >>> numbers = {10, 20, 30, 40}
-- >>> print(numbers[1])  -- Primer elemento
-- 10
-- >>> print(numbers[0])  -- Esto es nil
-- nil
-- >>> print(numbers[5])  -- Esto también es nil
-- nil

¿Por qué? Roberto Ierusalimschy (creador de Lua) diseñó Lua pensando en no-programadores. En matemáticas y en el lenguaje natural, contamos desde 1, no desde 0.

SOAPBOX: Índices desde 1

Sí, la mayoría de lenguajes usan índices desde 0. Pero Lua fue diseñado para ser embebido en aplicaciones, frecuentemente usado por diseñadores de juegos, artistas, y científicos que no son programadores profesionales. Para ellos, índices desde 1 es más natural.

Si vienes de Python, JavaScript, o C, te adaptarás en un día. Dale una oportunidad.

El Operador #: Longitud de un Array

El operador # devuelve la longitud de un array:

-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> print(#fruits)
-- 3

Pero hay una trampa importante: # solo funciona correctamente con secuencias.

Secuencias vs Tablas Sparse

Una secuencia es una tabla cuyos índices son enteros consecutivos desde 1 hasta N, sin huecos (nil).

-- Esto es una secuencia
-- >>> seq = {10, 20, 30, 40}
-- >>> print(#seq)
-- 4

-- Esto NO es una secuencia (tiene un hueco)
-- >>> sparse = {10, 20, nil, 40}
-- >>> print(#sparse)
-- 2  -- ¡Se detiene en el primer nil!

Si una tabla tiene huecos, el comportamiento de # es indefinido. Puede devolver cualquier valor válido como “longitud”.

-- >>> weird = {10, 20, nil, 40, 50}
-- >>> print(#weird)
-- 2  -- Puede ser 2, o 5, o cualquier cosa

ADVERTENCIA: Huecos en Arrays

Evita crear arrays con nil en el medio. Si necesitas “vacío”, usa un valor centinela como false o una string vacía "".

Agregar Elementos: table.insert

Para agregar elementos al final de un array, usa table.insert:

-- >>> fruits = {'manzana', 'banana'}
-- >>> table.insert(fruits, 'cereza')
-- >>> print(#fruits)
-- 3
-- >>> print(fruits[3])
-- cereza

También puedes insertar en una posición específica:

-- >>> table.insert(fruits, 2, 'arándano')  -- Insertar en posición 2
-- >>> for i, fruit in ipairs(fruits) do
-- >>>     print(i, fruit)
-- >>> end
-- 1    manzana
-- 2    arándano
-- 3    banana
-- 4    cereza

Eliminar Elementos: table.remove

Para eliminar el último elemento:

-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> local last = table.remove(fruits)
-- >>> print(last)
-- cereza
-- >>> print(#fruits)
-- 2

O eliminar de una posición específica:

-- >>> table.remove(fruits, 1)  -- Eliminar primer elemento
-- >>> print(fruits[1])
-- banana

NOTA: Performance de table.insert y table.remove

Insertar/eliminar al final es O(1). Insertar/eliminar en el medio es O(n) porque requiere desplazar elementos.

Iteración: ipairs vs pairs

Hay dos formas de iterar sobre una tabla en Lua: ipairs y pairs.

ipairs: Para Secuencias

ipairs itera sobre una secuencia en orden, desde 1 hasta el primer nil:

-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> for i, fruit in ipairs(fruits) do
-- >>>     print(i, fruit)
-- >>> end
-- 1    manzana
-- 2    banana
-- 3    cereza

Si hay un hueco, ipairs se detiene:

-- >>> sparse = {10, 20, nil, 40}
-- >>> for i, v in ipairs(sparse) do
-- >>>     print(i, v)
-- >>> end
-- 1    10
-- 2    20
-- (se detiene en el nil)

pairs: Para Todas las Claves

pairs itera sobre todas las claves de una tabla, sin importar si es secuencia o no:

-- >>> mixed = {10, 20, 30, foo = 'bar', baz = 42}
-- >>> for k, v in pairs(mixed) do
-- >>>     print(k, v)
-- >>> end
-- 1    10
-- 2    20
-- 3    30
-- foo    bar
-- baz    42

El orden de pairs es indefinido. No confíes en él.

REGLA DE ORO:

  • Usa ipairs cuando trabajes con secuencias puras.
  • Usa pairs cuando trabajes con tablas que pueden tener claves no-numéricas.

Operaciones Comunes con Arrays

Concatenar: table.concat

Para unir elementos de un array en una string:

-- >>> words = {'Lua', 'is', 'awesome'}
-- >>> print(table.concat(words, ' '))
-- Lua is awesome

-- >>> numbers = {1, 2, 3, 4, 5}
-- >>> print(table.concat(numbers, ', '))
-- 1, 2, 3, 4, 5

Puedes especificar un rango:

-- >>> print(table.concat(numbers, ', ', 2, 4))  -- Desde índice 2 al 4
-- 2, 3, 4

PERFORMANCE TIP:

Usar table.concat es mucho más eficiente que concatenar strings con .. en un loop:

-- ❌ LENTO
local result = ""
for _, word in ipairs(words) do
    result = result .. word .. " "
end

-- ✅ RÁPIDO
local result = table.concat(words, " ")

¿Por qué? Las strings en Lua son inmutables. Cada concatenación crea una nueva string.

Ordenar: table.sort

Para ordenar un array in-place:

-- >>> numbers = {5, 2, 8, 1, 9}
-- >>> table.sort(numbers)
-- >>> for _, n in ipairs(numbers) do print(n) end
-- 1
-- 2
-- 5
-- 8
-- 9

Con un comparador custom:

-- >>> table.sort(numbers, function(a, b) return a > b end)  -- Orden descendente
-- >>> for _, n in ipairs(numbers) do print(n) end
-- 9
-- 8
-- 5
-- 2
-- 1

Para strings:

-- >>> fruits = {'cereza', 'manzana', 'banana'}
-- >>> table.sort(fruits)
-- >>> for _, fruit in ipairs(fruits) do print(fruit) end
-- banana
-- cereza
-- manzana

Buscar Elementos

Lua no tiene un método built-in para buscar, pero es fácil de implementar:

local function find(tbl, value)
    for i, v in ipairs(tbl) do
        if v == value then
            return i
        end
    end
    return nil
end

-- >>> numbers = {10, 20, 30, 40}
-- >>> print(find(numbers, 30))
-- 3
-- >>> print(find(numbers, 99))
-- nil

Tablas Multidimensionales

Lua no tiene arrays multidimensionales nativos, pero puedes simularlos con tablas de tablas:

-- Matriz 3x3
-- >>> matrix = {
-- >>>     {1, 2, 3},
-- >>>     {4, 5, 6},
-- >>>     {7, 8, 9}
-- >>> }
-- >>> print(matrix[2][3])  -- Fila 2, columna 3
-- 6

Iterar sobre una matriz:

-- >>> for i, row in ipairs(matrix) do
-- >>>     for j, value in ipairs(row) do
-- >>>         print(string.format('matrix[%d][%d] = %d', i, j, value))
-- >>>     end
-- >>> end
-- matrix[1][1] = 1
-- matrix[1][2] = 2
-- matrix[1][3] = 3
-- matrix[2][1] = 4
-- ...

Alternativa: Tablas de 1 Dimensión

Para mejor performance, usa una tabla 1D con índices calculados:

local function create_matrix(rows, cols, initial_value)
    local m = {}
    for i = 1, rows * cols do
        m[i] = initial_value or 0
    end
    return {
        data = m,
        rows = rows,
        cols = cols,
        get = function(self, i, j)
            return self.data[(i - 1) * self.cols + j]
        end,
        set = function(self, i, j, value)
            self.data[(i - 1) * self.cols + j] = value
        end
    }
end

-- >>> m = create_matrix(3, 3, 0)
-- >>> m:set(2, 3, 42)
-- >>> print(m:get(2, 3))
-- 42

Esta técnica es más eficiente porque tiene mejor localidad de cache.

Caso Práctico: Implementar una Cola Circular

Una cola circular (circular queue) es una estructura de datos eficiente para implementar una cola (FIFO) con espacio fijo.

local CircularQueue = {}
CircularQueue.__index = CircularQueue

function CircularQueue.new(capacity)
    local self = setmetatable({}, CircularQueue)
    self.capacity = capacity
    self.data = {}
    self.head = 1
    self.tail = 1
    self.size = 0
    return self
end

function CircularQueue:enqueue(value)
    if self.size == self.capacity then
        error("Queue is full")
    end

    self.data[self.tail] = value
    self.tail = (self.tail % self.capacity) + 1
    self.size = self.size + 1
end

function CircularQueue:dequeue()
    if self.size == 0 then
        error("Queue is empty")
    end

    local value = self.data[self.head]
    self.data[self.head] = nil  -- Ayudar al GC
    self.head = (self.head % self.capacity) + 1
    self.size = self.size - 1
    return value
end

function CircularQueue:peek()
    if self.size == 0 then
        return nil
    end
    return self.data[self.head]
end

function CircularQueue:is_empty()
    return self.size == 0
end

function CircularQueue:is_full()
    return self.size == self.capacity
end

function CircularQueue:__len()
    return self.size
end

function CircularQueue:__tostring()
    local elements = {}
    local index = self.head
    for i = 1, self.size do
        table.insert(elements, tostring(self.data[index]))
        index = (index % self.capacity) + 1
    end
    return "CircularQueue[" .. table.concat(elements, ", ") .. "]"
end

Uso:

-- >>> q = CircularQueue.new(5)
-- >>> q:enqueue(10)
-- >>> q:enqueue(20)
-- >>> q:enqueue(30)
-- >>> print(q)
-- CircularQueue[10, 20, 30]
-- >>> print(q:dequeue())
-- 10
-- >>> print(q:peek())
-- 20
-- >>> print(#q)
-- 2

DEEP DIVE: Cómo Funcionan las Tablas Internamente

Las tablas de Lua tienen una implementación híbrida: parte array, parte hash table.

La Parte Array

Cuando Lua detecta que estás usando índices consecutivos desde 1, optimiza la tabla para usar un array real en memoria:

-- >>> t = {10, 20, 30, 40, 50}  -- Optimizado como array

Esto hace que el acceso sea O(1) y muy eficiente en cache.

La Parte Hash

Cuando usas claves no-enteras o índices no-consecutivos, Lua usa una hash table:

-- >>> t = {foo = 'bar', baz = 42}  -- Hash table

Híbrido

La magia ocurre cuando mezclas ambos:

-- >>> mixed = {10, 20, 30, foo = 'bar'}
-- Los índices 1, 2, 3 van al array
-- La clave 'foo' va al hash

Lua decide dinámicamente cuánto espacio asignar a cada parte basándose en tu uso.

Implicaciones para Performance

  1. Secuencias puras son las más rápidas: Usa índices consecutivos desde 1 siempre que puedas.

  2. Evita huecos: Un solo nil en el medio puede hacer que Lua cambie a hash table.

  3. Prealoca si conoces el tamaño: Aunque Lua crece dinámicamente, preallocating es más eficiente:

-- En lugar de:
local t = {}
for i = 1, 1000 do
    t[i] = i * 2
end

-- Mejor (solo en LuaJIT con tabla grande):
local t = table.new(1000, 0)  -- LuaJIT: 1000 array slots, 0 hash slots
for i = 1, 1000 do
    t[i] = i * 2
end

Cuándo NO Usar Arrays

Arrays no son la mejor estructura para todo:

❌ No uses arrays para:

✅ Usa arrays para:

En el próximo capítulo, veremos cómo usar tablas como diccionarios y sets.


Resumen del Capítulo

Las tablas de Lua son increíblemente versátiles. Como arrays:

Internamente, las tablas son híbridas: parte array (rápido), parte hash (flexible).

Próximo: Capítulo 3: Diccionarios y Conjuntos


Ejercicios

  1. Array Reverso: Escribe una función que revierta un array in-place.
function reverse(arr)
    -- Tu código aquí
end

-- >>> nums = {1, 2, 3, 4, 5}
-- >>> reverse(nums)
-- >>> print(table.concat(nums, ', '))
-- 5, 4, 3, 2, 1
  1. Flatten: Escribe una función que aplane un array anidado.
function flatten(arr)
    -- Tu código aquí
end

-- >>> nested = {1, {2, 3}, {4, {5, 6}}}
-- >>> flat = flatten(nested)
-- >>> print(table.concat(flat, ', '))
-- 1, 2, 3, 4, 5, 6
  1. Implementa un Stack: Usa una tabla para implementar un stack (push, pop, peek, is_empty).

Lecturas Adicionales