La biblioteca table
Insertar, quitar, concatenar, ordenar, mover y desempaquetar secuencias con la biblioteca estándar de Lua: el coste real de cada operación, el contrato que todas comparten y qué le ocurre exactamente a cada una cuando la tabla deja de ser una secuencia por culpa de un agujero.
La biblioteca table es diminuta: siete funciones que caben en media pantalla de documentación. Esa brevedad engaña, porque todas comparten un contrato tácito que el manual enuncia una sola vez y del que depende por completo su corrección: operan sobre secuencias, es decir, sobre tablas cuyas claves enteras van de 1 a un cierto n sin ausencia alguna. En cuanto ese contrato se rompe, ninguna de ellas falla de forma ruidosa; simplemente empieza a devolver resultados que dependen de la disposición interna de la memoria. Este apartado enseña las siete y, sobre todo, dónde está su frontera.
- Usar insertar, quitar, concatenar, ordenar y mover con sus dos formas de invocación.
- Conocer el coste algorítmico real de cada operación y elegir la posición adecuada.
- Distinguir empaquetar de desempaquetar y su papel con funciones variádicas.
- Predecir qué hace cada función cuando la tabla contiene un agujero.
Insertar, quitar y mover
table.insert admite dos formas. Con dos argumentos añade al final; con tres, inserta en la posición indicada y desplaza hacia la derecha todo lo que había desde ahí. table.remove es su simétrica: devuelve el elemento extraído y compacta el hueco.
local t = { "a", "b", "c" }
table.insert(t, "d") -- al final: coste constante amortizado
table.insert(t, 1, "z") -- en la posicion 1: desplaza n elementos
--> { "z", "a", "b", "c", "d" }
local ultimo = table.remove(t) -- quita y devuelve el ultimo
local primero = table.remove(t, 1) -- quita el primero y compacta
print(ultimo, primero) --> d z
table.move es la más reciente y la menos conocida: copia un rango de una secuencia a otra posición, incluso dentro de la misma tabla, y maneja correctamente los solapamientos. Es la primitiva sobre la que se pueden construir rotaciones, desplazamientos e inserciones en bloque sin bucles a mano.
local origen = { 1, 2, 3, 4, 5 }
local destino = {}
table.move(origen, 2, 4, 1, destino) --> destino = { 2, 3, 4 }
table.move(origen, 1, 3, 3) --> origen = { 1, 2, 1, 2, 3 }
t[#t + 1] = v y table.insert(t, v) producen el mismo resultado, pero la primera evita una llamada a función y una comprobación de argumentos. En bucles calientes la diferencia es medible. La forma con table.insert gana en legibilidad cuando además hay inserciones en posiciones intermedias, porque mantiene una sola notación para todo el código.
Concatenar y ordenar
table.concat une los elementos de una secuencia en una sola cadena, con separador opcional y rango opcional. Es la forma canónica de construir texto en Lua: acumular fragmentos en una tabla y concatenar una sola vez al final evita la explosión cuadrática de crear una cadena nueva en cada paso, porque las cadenas de Lua son inmutables e internadas.
local partes = { "uno", "dos", "tres" }
print(table.concat(partes, ", ")) --> uno, dos, tres
print(table.concat(partes, " | ", 2, 3)) --> dos | tres
El rango opcional es más útil de lo que parece: permite reutilizar un único vector de fragmentos como acumulador reiniciable, uniendo solo la porción viva y evitando asignar una tabla nueva en cada pasada.
local buf, n = {}, 0
local function emitir(s) n = n + 1; buf[n] = s end
emitir("cabecera"); emitir("cuerpo"); emitir("pie")
local salida = table.concat(buf, "\n", 1, n)
n = 0 -- reinicio sin reasignar: la tabla se reaprovecha entera
table.sort ordena en el sitio, sin devolver nada, mediante una variante de ordenación rápida. No es estable: elementos considerados equivalentes pueden quedar en cualquier orden relativo entre ellos. El segundo argumento es una función de comparación que debe responder si el primer elemento va estrictamente antes que el segundo.
local nombres = { "carla", "ana", "beto" }
table.sort(nombres) -- orden natural
table.sort(nombres, function(a, b) return a > b end) -- descendente
Si la función de comparación devuelve verdadero cuando los dos elementos son equivalentes —el error clásico es escribir menor o igual en lugar de menor—, el algoritmo puede salirse del rango del vector y Lua aborta con un error de orden no válido. La regla es sencilla y no admite excepciones: el comparador debe ser irreflexivo y transitivo, y debe dar el mismo resultado cada vez que se le pregunte por el mismo par.
Empaquetar y desempaquetar
table.unpack convierte una secuencia en una lista de valores múltiples, y table.pack hace el camino inverso añadiendo un campo con el recuento real. Juntas son el puente entre el mundo de las tablas y el de las funciones variádicas.
local args = { 3, 5 }
print(math.max(table.unpack(args))) --> 5
local p = table.pack(1, nil, 3)
print(p.n, #p) --> 3 y un valor no fiable para la longitud
El campo con el recuento existe precisamente porque el operador de longitud deja de ser fiable en presencia de agujeros. Por eso el idioma robusto para reenviar argumentos arbitrarios, incluidos los ausentes, es empaquetar con recuento y desempaquetar indicando el rango completo de forma explícita:
local function envolver(f)
return function(...)
local p = table.pack(...)
-- aqui se podria registrar, validar o transformar p
return f(table.unpack(p, 1, p.n)) -- rango explicito: conserva los ausentes
end
end
table.unpack admite siempre un índice inicial y uno final; sin ellos usa 1 y la longitud, que es justo el valor poco fiable que queremos evitar. Conviene recordar además que el número de valores devueltos por esta función está acotado por el tamaño de la pila del intérprete: desempaquetar una secuencia de cientos de miles de elementos no es una operación válida, sino una vía rápida al desbordamiento.
flowchart TD A[Tabla con un agujero] --> B[Operador de longitud] A --> C[Insertar y quitar] A --> D[Concatenar] A --> E[Ordenar] B --> B1[Devuelve cualquier frontera valida] C --> C1[Comportamiento indefinido] D --> D1[Error al intentar unir un valor ausente] E --> E1[Error de comparacion con valor ausente]
Qué ocurre con un agujero
Un agujero es un valor ausente intercalado entre claves enteras. Basta uno para que la tabla deje de ser secuencia, y a partir de ahí cada función de la biblioteca se comporta de una manera distinta:
La longitud es una frontera
El operador de longitud no cuenta elementos: devuelve algún índice cuyo sucesor esté ausente. Con agujeros, cualquiera de esas fronteras es una respuesta legítima, y la elegida depende de la disposición interna.
Insertar y quitar se desorientan
Ambas se apoyan en la longitud para localizar el extremo. Sobre una tabla con agujeros la posición de trabajo se vuelve impredecible y la operación queda formalmente indefinida.
Concatenar falla ruidosamente
Al llegar al hueco intenta unir un valor ausente y aborta con un error que indica la posición exacta. Es la única de las cinco que avisa siempre.
Ordenar aborta al comparar
El comparador recibe el valor ausente y la comparación lanza un error de tipos. Si el comparador lo tolera por casualidad, el resultado es una permutación arbitraria.
La lección práctica es que en Lua no se borra un elemento intermedio asignando el valor ausente. Se borra con table.remove, que compacta, o bien se marca con un centinela y se compacta después en una sola pasada.
Comparar la biblioteca table con la de cualquier lenguaje contemporáneo es un ejercicio revelador. Donde otros ofrecen decenas de métodos sobre un tipo lista que garantiza sus propios invariantes, Lua ofrece siete funciones sobre un tipo que no garantiza ninguno, y traslada el invariante entero al programador en forma de una palabra del manual: secuencia. Esto no es descuido, es la consecuencia inevitable de la decisión estudiada en la primera lección. Si solo hay un tipo compuesto y ese tipo es una función parcial de claves a valores, entonces la noción de lista no puede ser un tipo verificado por el sistema; solo puede ser una convención sobre cómo se usan las claves. Y una convención no se puede comprobar en tiempo constante: averiguar si una tabla es una secuencia exigiría recorrerla entera, lo que convertiría cada llamada a la biblioteca en una operación lineal en el mejor caso. Lua se niega a pagar ese precio y en su lugar declara el comportamiento indefinido, que es la forma honesta de decir que la responsabilidad es tuya. Lo notable es que la disciplina resultante es sencilla de sostener: no dejes agujeros, borra con la función que compacta, empaqueta con recuento cuando los valores ausentes sean posibles y usa la longitud solo sobre tablas de las que sepas con certeza que están completas. Quien adopta esas cuatro reglas casi nunca se topa con lo indefinido; quien las ignora acaba depurando un programa cuyo comportamiento cambia al añadir una línea, porque una redistribución interna de la tabla movió la frontera que devolvía la longitud. La biblioteca no te protege, pero es perfectamente predecible en cuanto respetas su única precondición.
- Construye una secuencia de diez elementos, borra el quinto asignando el valor ausente y compara la longitud antes y después; repite con
table.remove. - Ordena una tabla con un comparador que use menor o igual y observa el error que produce el intérprete.
- Construye una cadena de mil fragmentos concatenando en un bucle y luego con
table.concat, y compara los tiempos. - Usa
table.movepara rotar una secuencia una posición a la izquierda sin crear ninguna tabla intermedia.