Varargs: los puntos suspensivos y las tablas con nombre de 5.5
Los varargs son la cara visible de la decisión de fondo de Lua: la lista de valores no es un dato, es un estado transitorio de la pila. Esta lección estudia primero la expresión de argumentos variables y demuestra que obedece exactamente la misma regla de ajuste que una llamada, después recorre las herramientas que la biblioteca estándar ofrece para interrogarla sin materializarla, el índice y la almohadilla de select, y las que la materializan y la devuelven a la pila, table.pack y table.unpack. A continuación se ataca el problema que arruina la lectura ingenua de los varargs, el de los nil intermedios, mostrando por qué contar con el operador de longitud sobre una tabla construida a mano da resultados indefinidos y por qué el campo de conteo explícito es la única respuesta correcta. La lección culmina en la novedad de Lua 5.5, publicada en diciembre de 2025: las tablas de varargs con nombre, que permiten declarar directamente la tabla de argumentos variables en la lista de parámetros y que convierten en sintaxis lo que durante treinta años fueron mecanismos indirectos.
Una función con varargs es una función cuyo último parámetro no es un parámetro sino un agujero por el que entra una lista de longitud desconocida. Esa lista, como la de resultados de la lección anterior, no es un valor: no tiene tipo, no se guarda, no se compara y no sobrevive fuera del instante en que se ajusta a un contexto. Trabajar con ella es, por tanto, un ejercicio de disciplina, porque cada operación que quieras hacer sobre los argumentos variables obliga a decidir si te basta con consumirlos sobre la marcha o si necesitas materializarlos en una tabla, con el coste y las trampas que eso trae. Durante tres décadas Lua ofreció para esto un conjunto de herramientas indirectas y perfectamente funcionales pero incómodas, y la incomodidad se notaba justo en el caso más frecuente: querer simplemente tener los argumentos extra en una tabla con su cuenta correcta. Lua 5.5, en diciembre de 2025, cerró ese hueco con las tablas de varargs con nombre, y esta lección termina ahí porque cambia cómo se escribe el idioma más común del lenguaje.
- Usar la expresión de varargs sabiendo que se expande solo en posición final, igual que una llamada.
- Interrogar la lista variable con
selectpor índice y con la almohadilla, sin construir tablas intermedias. - Materializar y devolver a la pila con
table.packytable.unpack, controlando los límites explícitamente. - Diagnosticar el problema de los
nilintermedios y resolverlo con el conteo explícito o con las tablas con nombre de 5.5.
La expresión de varargs y su regla de ajuste
Una función declara que acepta argumentos variables terminando su lista de parámetros con los puntos suspensivos. Dentro del cuerpo, esa misma escritura es una expresión que representa la lista de argumentos sobrantes, y obedece la regla de ajuste que ya conoces: se expande a todos sus valores solo si aparece como último elemento de una lista de expresiones, y en cualquier otra posición se recorta a uno.
local function traza(etiqueta, ...)
print(etiqueta, ...) -- ULTIMA: se expande a todos
local a, b = ... -- toma los dos primeros, rellena con nil
local t = { ... } -- constructor: se expande al final
return ... -- reenvia todos, sin saber cuantos son
end
traza("log", 10, 20, 30) --> log 10 20 30
-- Reenvio exacto a otra funcion: el idioma mas util del lenguaje
local function envolver(fn)
return function(...)
return fn(...) -- ni pierde ni anade resultados
end
end
-- Los varargs son POSICIONALES, no una tabla: no se puede indexar
-- print(...[1]) <- no existe tal cosa
La última línea comentada señala la incomodidad estructural: no hay forma de indexar directamente la lista variable ni de preguntarle su longitud con el operador habitual, porque no es una tabla. Todo lo que se puede hacer con ella pasa por alguna de las herramientas de la sección siguiente.
Interrogar y materializar
La biblioteca estándar ofrece tres funciones para esto, y conviene tener claro cuál asigna memoria y cuál no. La función select no construye nada: recorta la lista desde una posición o devuelve su longitud exacta. Las funciones table.pack y table.unpack son el par que convierte la lista en tabla y la tabla otra vez en lista.
local function inspeccionar(...)
print(select("#", ...)) -- CUANTOS hay, contando los nil
print(select(2, ...)) -- desde el segundo hasta el final
print((select(2, ...))) -- solo el segundo: parentesis que truncan
print(select(-1, ...)) -- indices negativos: el ultimo
end
-- Recorrer sin materializar nada
local function sumar(...)
local total = 0
for i = 1, select("#", ...) do total = total + select(i, ...) end
return total
end
-- Materializar: pack deja las posiciones 1 hasta n y el conteo en el campo n
local function guardar(...) return table.pack(...) end
-- unpack devuelve a la pila, con limites explicitos
local args = { 10, 20, 30 }
print(table.unpack(args)) --> 10 20 30
print(table.unpack(args, 1, 2)) --> 10 20
El bucle con select por índice es correcto pero cuadrático si la lista es larga, porque cada llamada vuelve a recorrer los argumentos. Para pocas posiciones es lo más barato; para recorrer todo, table.pack una sola vez sale mejor pese a la asignación.
El problema de los nil intermedios
Aquí está el error más difícil de diagnosticar de todo el nivel. Una tabla construida con el constructor a partir de varargs pierde la información de cuántos argumentos hubo si alguno de ellos fue nil, porque el operador de longitud sobre una tabla con agujeros no tiene un resultado definido: puede devolver cualquier frontera válida.
local function malo(...)
local t = { ... }
return #t -- INDEFINIDO si hay nil intermedios
end
print(malo("a", nil, "c")) --> puede dar 3, puede dar 1
local function bueno(...)
return select("#", ...) -- SIEMPRE exacto, cuenta los nil
end
print(bueno("a", nil, "c")) --> 3
local function tambienBueno(...)
return table.pack(...).n -- pack registra el conteo real
end
-- Y por eso unpack debe recibir el limite superior explicito
local t = table.pack("a", nil, "c")
print(table.unpack(t)) -- puede parar en el agujero
print(table.unpack(t, 1, t.n)) --> a nil c correcto siempre
La regla práctica es breve y no admite matices: en cuanto una función acepte varargs que puedan contener nil, el conteo debe venir de la almohadilla de select o del campo de conteo que escribe table.pack, nunca del operador de longitud.
Lua 5.5: tablas de varargs con nombre
La versión publicada en diciembre de 2025 añadió la pieza que faltaba. La lista de parámetros puede terminar con los puntos suspensivos seguidos de un nombre, y ese nombre queda ligado dentro del cuerpo a una tabla que contiene los argumentos sobrantes en las posiciones consecutivas y su conteo exacto en el campo de conteo. Es, en efecto, lo que table.pack construía, pero declarado en la firma y sin el paso intermedio.
-- Antes de 5.5: el idioma indirecto de siempre
local function registrar(nivel, ...)
local extras = table.pack(...)
for i = 1, extras.n do io.write(tostring(extras[i]), " ") end
end
-- En 5.5: la tabla de varargs se declara y se nombra en la firma
local function registrar(nivel, ...extras)
for i = 1, extras.n do io.write(tostring(extras[i]), " ") end
end
-- La tabla es un valor normal: se guarda, se pasa y se devuelve
local function memorizar(...args)
return function() return table.unpack(args, 1, args.n) end
end
-- Y conserva los nil intermedios con su conteo correcto
local function contar(...args) return args.n end
print(contar("a", nil, "c")) --> 3
Lo que cambia no es la capacidad sino el punto de la escritura donde se toma la decisión. Con los puntos suspensivos sin nombre, la firma no dice nada y cada cuerpo elige a su manera si consumir la lista al vuelo o materializarla; con la forma con nombre, la firma declara que esos argumentos van a existir como estructura y el lector lo sabe antes de entrar. La forma clásica no desaparece y sigue siendo la correcta cuando lo único que quieres es reenviar sin tocar, porque ahí materializar sería trabajo desperdiciado.
Misma regla de ajuste
Los varargs se expanden solo en posición final y se truncan entre paréntesis, exactamente igual que una llamada.
La almohadilla no miente
El conteo de select con la almohadilla es exacto siempre, incluidos los nil. El operador de longitud no lo es.
El par de empaquetado
table.pack materializa con conteo, table.unpack devuelve a la pila. Pasa siempre los límites explícitos.
Nombre en la firma
Lua 5.5 permite nombrar la tabla de varargs en la lista de parámetros, con su conteo incluido y sin llamada intermedia.
flowchart TD A[Argumentos sobrantes] --> B[Lista variable en la pila] B --> C[select con indice o almohadilla] B --> D[table pack materializa con conteo] D --> F[Tabla con campo n] F --> G[table unpack con limites explicitos] H[Lua 5 punto 5] --> F style F fill:#a6e3a1,color:#11111b style H fill:#89b4fa,color:#11111b
La historia de los varargs de Lua es un caso de estudio sobre cómo envejece un diseño minimalista y qué señales indican que hay que ceder. La decisión original fue impecable y coherente con el resto del lenguaje: la lista variable no se materializa porque materializarla cuesta una asignación de memoria y una visita posterior del recolector, y el uso mayoritario de los varargs, que es reenviarlos intactos a otra función, no necesita tabla ninguna. Bajo esa premisa, obligar a escribir una llamada de empaquetado explícita cuando sí querías la tabla era el precio justo por no penalizar al caso barato. El problema es que la premisa era solo medio cierta, y la mitad falsa se cobró treinta años de errores. Porque el segundo uso más común no era reenviar sino recorrer, y recorrer exige el conteo, y el conteo por el camino natural, construir la tabla con el constructor y medirla con el operador de longitud, es incorrecto en presencia de agujeros de una manera que no falla en las pruebas y sí en producción, cuando por fin llega un argumento nulo en medio. Lua respondió a eso en 5.2 con la función de empaquetado y su campo de conteo, que resolvía el fondo pero dejaba la solución correcta más larga de escribir que la incorrecta, y esa es exactamente la configuración que garantiza que la gente siga escribiendo la incorrecta. Las tablas de varargs con nombre de 5.5 invierten esa asimetría: ahora la forma que declara la intención, conserva los nulos y trae el conteo es también la más corta, y la que ya no aparece por accidente es la peligrosa. Lo interesante es lo que esto revela sobre el criterio de evolución del lenguaje. Lua no añadió una construcción nueva ni un tipo nuevo ni un mecanismo nuevo: tomó un patrón que la comunidad ya escribía a mano, comprobó que la versión manual tenía una variante incorrecta más cómoda que la correcta, y le dio sintaxis al resultado bueno. Es el mismo movimiento que en su día convirtió la asignación de una función a una variable en la forma con nombre, y el que convirtió la inserción de un primer argumento en los dos puntos. Un lenguaje que crece así no engorda: cada azúcar que añade elimina más código repetido y más errores de los que introduce en gramática. Esa es la diferencia entre añadir características y destilar idiomas, y explica por qué después de tres décadas el manual de Lua sigue cabiendo en una tarde.
- Escribe una función con varargs que imprima su conteo de las dos formas, con el operador de longitud sobre la tabla construida y con la almohadilla de
select. Llámala con un nulo en medio y compara. - Implementa un envoltorio que mida el tiempo de otra función y reenvíe argumentos y resultados sin alterar el número de ninguno. Demuéstralo con una función que devuelva tres valores.
- Guarda una lista variable dentro de un closure y devuélvela más tarde con
table.unpack, pasando y sin pasar los límites explícitos. Explica la diferencia con un nulo intermedio. - Reescribe dos de las funciones anteriores usando la tabla de varargs con nombre de 5.5 y compara líneas, legibilidad y comportamiento ante nulos.
- Busca en un proyecto real una función que use el constructor de tabla sobre varargs y determina si es vulnerable al problema del conteo. Propón el arreglo mínimo.