wandres.dev
FUNCIONES · valores de primera clase

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.

⏱ 19 min

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.

🎯 Al terminar esta lección sabrás
  • Usar la expresión de varargs sabiendo que se expande solo en posición final, igual que una llamada.
  • Interrogar la lista variable con select por índice y con la almohadilla, sin construir tablas intermedias.
  • Materializar y devolver a la pila con table.pack y table.unpack, controlando los límites explícitamente.
  • Diagnosticar el problema de los nil intermedios 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
Nombrar la tabla de varargs es admitir que el caso frecuente merecía sintaxis

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.

⚔️ Cuenta bien o falla en producción
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.