wandres.dev
FUNCIONES · valores de primera clase

Retornos múltiples y las reglas de ajuste

Lua es de los poquísimos lenguajes en los que una llamada no produce un valor sino una lista de valores de longitud arbitraria, y esa decisión, que parece una comodidad menor, reorganiza la semántica de la asignación, del constructor de tabla, de la lista de argumentos y hasta del bucle genérico. Esta lección estudia primero cómo se devuelven y se reciben varios valores, y después se concentra en lo que de verdad hay que dominar: las reglas de ajuste que deciden cuándo una llamada se expande a todos sus resultados y cuándo se recorta a uno solo. Se examinan las tres posiciones que importan, última de una lista, interior de una lista y envuelta en paréntesis, se explica por qué los paréntesis dejan de ser inertes en este único contexto del lenguaje, y se revisan los idiomas que la biblioteca estándar construyó sobre este mecanismo, del par valor y error al conteo exacto con la almohadilla. La tesis final es que el retorno múltiple no es una lista ligera: es la ausencia deliberada de una estructura de datos allí donde otros lenguajes se ven obligados a inventarla.

⏱ 18 min

Pregúntale a alguien que conozca Lua de pasada qué lo distingue y probablemente responda las tablas o las metatablas. Pregúntale a alguien que lo haya usado en serio y la respuesta cambia: lo que separa a Lua del resto es que una llamada no devuelve un valor, devuelve una lista de valores cuya longitud puede ser cero, uno o muchos, y esa lista se propaga por el programa hasta que algún contexto decide cuántos elementos quiere. Ningún lenguaje de la familia dominante hace esto. Los que devuelven varias cosas construyen una tupla, un objeto, un registro o una estructura, es decir, un valor único que empaqueta a los demás y que hay que desempaquetar en destino. Lua no empaqueta nada: la lista de resultados no es un valor, no tiene tipo, no se puede guardar en una variable y no existe fuera del instante en que se produce. Entender ese estatuto fantasmal, y las reglas que lo gobiernan, es la diferencia entre escribir Lua y escribir otro lenguaje con sintaxis de Lua.

🎯 Al terminar esta lección sabrás
  • Devolver y recibir listas de valores de longitud arbitraria, incluyendo el caso de cero resultados.
  • Aplicar sin dudar las reglas de ajuste según la posición: última de una lista, interior de una lista, o envuelta en paréntesis.
  • Explicar por qué los paréntesis, que en el resto del lenguaje solo agrupan, aquí truncan a un valor.
  • Reconocer y usar los idiomas de la biblioteca estándar levantados sobre el retorno múltiple.

Devolver y recibir una lista de valores

Devolver varios valores no requiere sintaxis nueva: se separan por comas después de return. Recibirlos tampoco: se separan por comas a la izquierda de la asignación. Lo único que hay que aprender es qué ocurre cuando los números no coinciden, y la respuesta es que Lua nunca falla por descuadre, sino que ajusta rellenando con nil o descartando lo que sobra.

local function dividir(a, b)
  if b == 0 then return nil, "division por cero" end
  return a // b, a % b
end

local coc, resto = dividir(17, 5)      --> 3   2
local solo      = dividir(17, 5)       --> 3        el resto se descarta
local c, r, x   = dividir(17, 5)       --> 3   2   nil

-- Cero valores tambien es una lista valida
local function nada() return end
local v = nada()                        --> nil

-- Y el idioma canonico de errores del lenguaje entero
local ok, err = dividir(1, 0)
if not ok then print(err) end

Este ajuste silencioso es la razón de que en Lua no exista el error de aridad ni en los retornos ni en los argumentos. El lenguaje trata las listas de valores como listas de longitud variable en todos los contextos y las adapta a lo que el destino necesita, exactamente igual que hace con los parámetros que faltan o que sobran.

Las tres reglas de ajuste

Aquí está el núcleo de la lección, y conviene enunciarlo como una regla única con tres consecuencias. Una llamada a función se expande a todos sus resultados si y solo si aparece como último elemento de una lista de expresiones. En cualquier otra posición se ajusta a exactamente un valor. Las listas de expresiones donde esto se aplica son cuatro: la lista de la derecha de una asignación, la lista de argumentos de otra llamada, la lista de valores de un return y el interior de un constructor de tabla.

local function tres() return 1, 2, 3 end

-- ULTIMA de la lista: se expande a todo
local a, b, c = tres()          --> 1  2  3
print(tres())                   --> 1  2  3
local t = { tres() }            --> t tiene 3 elementos

-- INTERIOR de la lista: se recorta a uno
local x, y = tres(), 99         --> x vale 1, y vale 99
print(tres(), "fin")            --> 1  fin
local u = { tres(), tres() }    --> 4 elementos: 1, 1, 2, 3

-- ENVUELTA EN PARENTESIS: se recorta a uno, aunque sea la ultima
print((tres()))                 --> 1
local w = { (tres()) }          --> 1 elemento

-- Combinaciones que conviene leer despacio
print("n:", tres())             --> n:  1  2  3
print(tres(), tres())           --> 1  1  2  3
local p = { tres(), n = 0 }     --> el campo con nombre no cierra la lista

La tercera regla es la que más sorprende. En todo el resto del lenguaje los paréntesis solo agrupan y son semánticamente inertes; aquí, y solo aquí, tienen un efecto observable: fuerzan el ajuste a un valor. No es una excepción arbitraria sino la única forma sintáctica disponible para decir quiero el primero y nada más sin introducir una variable temporal.

⚠️
Los tres puntos y la última posición son la misma regla

La expansión no es una propiedad de las llamadas sino de las llamadas y de la expresión de varargs, y ambas obedecen la regla idéntica. Los puntos suspensivos también se expanden solo en posición final y también se truncan entre paréntesis. Si internalizas la regla como las expresiones de longitud variable se expanden únicamente al final, no necesitas memorizar dos comportamientos: hay uno solo con dos formas de escribirse.

Los idiomas que este mecanismo hizo posibles

El retorno múltiple no es una curiosidad sintáctica; la biblioteca estándar entera está diseñada alrededor de él, hasta el punto de que buena parte de sus funciones resultarían incómodas sin él. Conviene conocer los patrones porque los vas a encontrar en cualquier código real.

-- 1. Valor y error, sin excepciones ni tipos suma
local f, err = io.open("/no/existe")
if not f then return nil, err end

-- 2. Reenviar TODOS los resultados sin saber cuantos son
local function envolver(fn, ...)
  return fn(...)              -- devuelve exactamente lo que devuelva fn
end

-- 3. Contar resultados sin materializar una tabla
print(select("#", tres()))    --> 3
print(select("#", nada()))    --> 0

-- 4. Buscar y capturar en una sola llamada
local ini, fin = string.find("hola mundo", "mundo")
local dia, mes = ("31-12"):match("(%d+)-(%d+)")

-- 5. El bucle generico consume tres valores de una sola llamada
for clave, valor in pairs(t) do end   -- pairs devuelve iterador, estado y control

-- 6. El truco de gsub: parentesis para quedarse solo con el texto
local limpio = (("  hola  "):gsub("%s+", ""))

El último ejemplo es un clásico y merece una lectura atenta. La función de sustitución devuelve dos valores, el texto resultante y el número de reemplazos; si la usas como último elemento de un return o dentro de un constructor de tabla, ese segundo valor se cuela sin que lo hayas pedido. Los paréntesis lo cortan, y por eso aparecen en tantísimo código de calidad envolviendo llamadas que a primera vista no los necesitan.

🧮

Una regla, no una lista

Se expande al final, se recorta en cualquier otro sitio. Todo lo demás son consecuencias de esa única frase.

🎯

El paréntesis que sí hace algo

Envolver una llamada la trunca a un valor. Es el único contexto del lenguaje donde agrupar tiene efecto observable.

🧾

Sin tupla intermedia

La lista de resultados no es un valor: no se guarda, no tiene tipo y no sobrevive al instante del ajuste.

⚠️

El segundo valor invisible

Muchas funciones estándar devuelven un extra informativo que se cuela en tablas y retornos si no lo truncas.

flowchart TD
A[Llamada a funcion] --> B{Posicion en la lista}
B -->|Ultima| C[Se expande a todos los resultados]
B -->|Interior| D[Se ajusta a un valor]
B -->|Entre parentesis| E[Se ajusta a un valor]
C --> F[Asignacion argumentos return o constructor]
style C fill:#a6e3a1,color:#11111b
style D fill:#f9e2af,color:#11111b
style E fill:#f38ba8,color:#11111b
No haber inventado la tupla es lo que hizo barato todo lo demás

La tentación al describir el retorno múltiple es presentarlo como una tupla ligera, y esa lectura es cómoda y equivocada. Una tupla es un valor: ocupa memoria, tiene un tipo, se asigna a una variable, se guarda en una estructura, se pasa entera y en algún momento hay que destruirla o recolectarla. La lista de resultados de Lua no es nada de eso. Vive en la pila de la máquina virtual entre el instante en que la función retorna y el instante en que el contexto receptor decide cuántos valores quiere, y en ese trayecto no se construye ningún objeto en el montón. Por eso devolver dos valores en Lua no cuesta prácticamente nada, mientras que devolver una tupla en un lenguaje que la materializa cuesta una asignación de memoria que después habrá que liberar. Esa gratuidad es lo que permitió que el diseño de la biblioteca estándar tomara decisiones que en otro lenguaje habrían sido caras o feas. Permitió el par de valor y mensaje de error sin necesidad de excepciones, sin tipos suma y sin envolver cada resultado en un contenedor. Permitió que el protocolo del bucle genérico consista en que una única llamada devuelva tres cosas, que es lo que hace que escribir un iterador propio sea escribir una función y no implementar una interfaz. Permitió que la expresión de varargs y la llamada compartan exactamente las mismas reglas de ajuste, de modo que reenviar argumentos hacia otra función preservando su número exacto se escriba en una línea sin empaquetar y desempaquetar. Y permitió que el lenguaje no necesite sintaxis de desestructuración, porque la asignación múltiple ya la cubre en el único caso que aparece de verdad. El precio, y hay precio, es que esa lista fantasmal no se puede guardar: en el momento en que quieras conservarla tendrás que materializarla en una tabla, con lo que vuelven el coste y el recolector, y ahí es donde aparecen las herramientas de la lección de varargs. Pero la asimetría es deliberada y está bien elegida, porque el caso frecuente es consumir los resultados inmediatamente y el caso raro es almacenarlos. Lua optimizó el frecuente hasta hacerlo gratis y dejó el raro explícito. Ese es, en miniatura, el criterio de diseño del lenguaje entero.

⚔️ Predice el ajuste antes de ejecutarlo
  1. Escribe una función que devuelva tres valores y otra que no devuelva ninguno. Colócalas en las cuatro listas de expresiones posibles y anota tu predicción antes de ejecutar cada caso.
  2. Construye una tabla cuyo constructor contenga la misma llamada dos veces y explica con la regla por qué el número final de elementos no es seis.
  3. Usa la almohadilla de select para contar los resultados de una llamada, después envuélvela en paréntesis y vuelve a contar.
  4. Toma una llamada a la función de sustitución de cadenas y úsala sin paréntesis dentro de un constructor de tabla. Inspecciona el resultado y explica de dónde salió el número.
  5. Escribe una función de envoltura que reciba otra función y le reenvíe todo, y comprueba con el conteo que no pierde ni añade resultados.
  6. Encuentra en código real un caso donde unos paréntesis aparentemente redundantes estén truncando una llamada, y razona qué pasaría si los quitaras.