Capítulo 5: Múltiples Valores y Destructuring
Capítulo 5: Múltiples Valores y Destructuring
“La elegancia no es opcional.” — Richard O’Brien
Una de las características más elegantes de Lua es su manejo de múltiples valores de retorno. A diferencia de lenguajes que requieren tuplas, arrays, o estructuras para retornar múltiples valores, Lua lo hace de forma nativa y natural.
Return Múltiple
En Lua, una función puede retornar varios valores separados por comas:
function get_coordinates()
return 10, 20 -- Retorna DOS valores
end
-- >>> x, y = get_coordinates()
-- >>> print(x, y)
-- 10 20
Capturar Solo Algunos Valores
Puedes ignorar valores que no necesitas:
function get_user_info()
return "Alice", 30, "[email protected]"
end
-- Capturar solo nombre y email
-- >>> name, _, email = get_user_info()
-- >>> print(name, email)
-- Alice [email protected]
Capturar Todos en una Tabla
Si quieres todos los valores en una tabla:
-- >>> values = {get_user_info()}
-- >>> print(values[1], values[2], values[3])
-- Alice 30 [email protected]
NOTA IMPORTANTE:
Solo las llamadas a funciones en la última posición de una expresión retornan múltiples valores:
function nums() return 1, 2, 3 end -- ✅ Última posición: retorna todos -- >>> x, y, z = nums() -- >>> print(x, y, z) -- 1 2 3 -- ❌ No es última posición: retorna solo el primero -- >>> x, y, z = nums(), 0 -- >>> print(x, y, z) -- 1 0 nil
Asignación Paralela
Lua soporta asignación múltiple en una sola línea:
-- >>> a, b, c = 1, 2, 3
-- >>> print(a, b, c)
-- 1 2 3
El Famoso Swap Sin Variable Temporal
Esto permite el truco más elegante de Lua:
-- >>> a, b = 1, 2
-- >>> a, b = b, a -- ¡Swap!
-- >>> print(a, b)
-- 2 1
¿Cómo funciona? Lua evalúa todas las expresiones del lado derecho ANTES de asignar:
-- Esto:
a, b = b, a
-- Internamente es:
temp_b = b
temp_a = a
a = temp_b
b = temp_a
Rotar Valores
-- >>> a, b, c = 1, 2, 3
-- >>> a, b, c = b, c, a -- Rotar
-- >>> print(a, b, c)
-- 2 3 1
Valores Faltantes y Extra
Si faltan valores, se rellenan con nil:
-- >>> x, y, z = 1, 2
-- >>> print(x, y, z)
-- 1 2 nil
Si sobran valores, se descartan:
-- >>> x, y = 1, 2, 3, 4
-- >>> print(x, y)
-- 1 2
Varargs: Número Variable de Argumentos
Usa ... para aceptar cualquier número de argumentos:
function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
-- >>> print(sum(1, 2, 3))
-- 6
-- >>> print(sum(10, 20, 30, 40, 50))
-- 150
Acceder a Varargs
Hay tres formas:
1. Convertir a Tabla
function print_all(...)
local args = {...}
for i, v in ipairs(args) do
print(i, v)
end
end
-- >>> print_all('a', 'b', 'c')
-- 1 a
-- 2 b
-- 3 c
2. Usar select
select(n, ...) devuelve todos los argumentos desde la posición n:
function first_and_rest(...)
local first = select(1, ...)
local rest = {select(2, ...)}
return first, rest
end
-- >>> first, rest = first_and_rest('a', 'b', 'c', 'd')
-- >>> print(first)
-- a
-- >>> print(table.concat(rest, ', '))
-- b, c, d
select('#', ...) devuelve el número de argumentos:
function count_args(...)
return select('#', ...)
end
-- >>> print(count_args('a', 'b', 'c'))
-- 3
-- >>> print(count_args())
-- 0
IMPORTANTE:
#{...}vsselect('#', ...)function test(...) print(#{...}) -- ❌ NO cuenta nil correctamente print(select('#', ...)) -- ✅ Cuenta todo, incluyendo nil end -- >>> test(1, nil, 3) -- 1 -- #{...} se detiene en el primer nil -- 3 -- select('#', ...) cuenta todos
3. Pasar Directamente
Puedes pasar varargs a otra función:
function wrapper(...)
return another_function(...)
end
Table Unpacking
table.unpack (en Lua 5.2+) o unpack (en Lua 5.1) convierte una tabla en múltiples valores:
-- >>> t = {10, 20, 30}
-- >>> print(table.unpack(t))
-- 10 20 30
Útil para pasar una tabla como argumentos:
function add(a, b, c)
return a + b + c
end
-- >>> numbers = {5, 10, 15}
-- >>> print(add(table.unpack(numbers)))
-- 30
Con rango:
-- >>> t = {1, 2, 3, 4, 5}
-- >>> print(table.unpack(t, 2, 4)) -- Desde índice 2 al 4
-- 2 3 4
Patrones Idiomáticos
Default Values
function greet(name)
name = name or "Guest"
print("Hello, " .. name)
end
-- >>> greet("Alice")
-- Hello, Alice
-- >>> greet()
-- Hello, Guest
Con múltiples defaults:
function create_user(name, age, city)
name = name or "Unknown"
age = age or 18
city = city or "Unknown"
return {name = name, age = age, city = city}
end
-- >>> user = create_user("Bob", nil, "Madrid")
-- >>> print(user.name, user.age, user.city)
-- Bob 18 Madrid
Patrón Options Table
Mejor que muchos parámetros opcionales:
function create_window(options)
options = options or {}
local width = options.width or 800
local height = options.height or 600
local title = options.title or "Window"
local resizable = options.resizable ~= false -- Default true
return {
width = width,
height = height,
title = title,
resizable = resizable
}
end
-- >>> window = create_window({title = "My App", width = 1024})
-- >>> print(window.title, window.width, window.height)
-- My App 1024 600
Builder Pattern con Return Self
local QueryBuilder = {}
QueryBuilder.__index = QueryBuilder
function QueryBuilder.new()
return setmetatable({
_table = nil,
_where = {},
_order = nil,
_limit = nil
}, QueryBuilder)
end
function QueryBuilder:from(table_name)
self._table = table_name
return self -- ¡Retornar self para chainear!
end
function QueryBuilder:where(condition)
table.insert(self._where, condition)
return self
end
function QueryBuilder:order_by(column)
self._order = column
return self
end
function QueryBuilder:limit(n)
self._limit = n
return self
end
function QueryBuilder:build()
local parts = {"SELECT * FROM " .. self._table}
if #self._where > 0 then
table.insert(parts, "WHERE " .. table.concat(self._where, " AND "))
end
if self._order then
table.insert(parts, "ORDER BY " .. self._order)
end
if self._limit then
table.insert(parts, "LIMIT " .. self._limit)
end
return table.concat(parts, " ")
end
Uso:
-- >>> query = QueryBuilder.new()
-- >>> :from("users")
-- >>> :where("age > 18")
-- >>> :where("city = 'Madrid'")
-- >>> :order_by("name")
-- >>> :limit(10)
-- >>> :build()
-- >>> print(query)
-- SELECT * FROM users WHERE age > 18 AND city = 'Madrid' ORDER BY name LIMIT 10
Caso Práctico: Router HTTP
Un router que retorna múltiples valores (status, headers, body):
local Router = {}
Router.__index = Router
function Router.new()
return setmetatable({routes = {}}, Router)
end
function Router:add_route(method, path, handler)
if not self.routes[method] then
self.routes[method] = {}
end
self.routes[method][path] = handler
end
function Router:handle(method, path, ...)
if not self.routes[method] then
return 404, {["Content-Type"] = "text/plain"}, "Not Found"
end
local handler = self.routes[method][path]
if not handler then
return 404, {["Content-Type"] = "text/plain"}, "Not Found"
end
-- Los handlers retornan (status, headers, body)
return handler(...)
end
-- Métodos helper
function Router:get(path, handler)
self:add_route("GET", path, handler)
end
function Router:post(path, handler)
self:add_route("POST", path, handler)
end
Uso:
-- >>> router = Router.new()
-- >>> router:get("/users", function()
-- >>> return 200, {["Content-Type"] = "application/json"}, '{"users": []}'
-- >>> end)
-- >>> router:get("/users/:id", function(id)
-- >>> return 200, {["Content-Type"] = "application/json"},
-- >>> string.format('{"id": "%s"}', id)
-- >>> end)
-- >>> router:post("/users", function(body)
-- >>> return 201, {["Content-Type"] = "application/json"},
-- >>> '{"created": true}'
-- >>> end)
-- >>> status, headers, body = router:handle("GET", "/users")
-- >>> print(status, body)
-- 200 {"users": []}
Caso Práctico: Validador de Datos
Funciones que retornan (success, value_or_error):
local Validator = {}
function Validator.required(value, field_name)
if value == nil or value == "" then
return false, field_name .. " is required"
end
return true, value
end
function Validator.min_length(value, min, field_name)
if #value < min then
return false, field_name .. " must be at least " .. min .. " characters"
end
return true, value
end
function Validator.max_length(value, max, field_name)
if #value > max then
return false, field_name .. " must be at most " .. max .. " characters"
end
return true, value
end
function Validator.email(value, field_name)
if not value:match("^[%w._%%-]+@[%w._%%-]+%.%a+$") then
return false, field_name .. " must be a valid email"
end
return true, value
end
function Validator.validate(value, validators)
for _, validator_fn in ipairs(validators) do
local ok, result = validator_fn(value)
if not ok then
return false, result -- result es el mensaje de error
end
end
return true, value
end
Uso:
-- >>> local email = "[email protected]"
-- >>> local ok, result = Validator.validate(email, {
-- >>> function(v) return Validator.required(v, "Email") end,
-- >>> function(v) return Validator.email(v, "Email") end,
-- >>> })
-- >>> print(ok, result)
-- true [email protected]
-- >>> email = "invalid"
-- >>> ok, error_msg = Validator.validate(email, {
-- >>> function(v) return Validator.required(v, "Email") end,
-- >>> function(v) return Validator.email(v, "Email") end,
-- >>> })
-- >>> print(ok, error_msg)
-- false Email must be a valid email
Trucos Avanzados
Ignorar Todos los Valores Excepto el Último
function get_many()
return 1, 2, 3, 4, 5
end
-- >>> last = select(-1, get_many())
-- >>> print(last)
-- 5
Concatenar Múltiples Returns
function func1() return 'a', 'b' end
function func2() return 'c', 'd' end
-- >>> result = {func1(), func2()}
-- >>> print(table.concat(result, ', '))
-- a, c
-- (solo toma el primer valor de cada función)
-- Para capturar todos:
-- >>> result = {}
-- >>> for _, v in ipairs({func1()}) do table.insert(result, v) end
-- >>> for _, v in ipairs({func2()}) do table.insert(result, v) end
-- >>> print(table.concat(result, ', '))
-- a, b, c, d
Función Variádica que Retorna Variadic
function multiply_all_by_2(...)
local results = {}
for _, v in ipairs({...}) do
table.insert(results, v * 2)
end
return table.unpack(results)
end
-- >>> print(multiply_all_by_2(1, 2, 3, 4))
-- 2 4 6 8
Resumen del Capítulo
Múltiples valores en Lua:
- Return múltiple:
return a, b, c - Asignación paralela:
a, b = b, a(swap sin temporal) - Varargs:
function f(...) end select('#', ...)para contar args (incluyenil)table.unpack(t)para convertir tabla a múltiples valores- Patrón (ok, result): Para manejo de errores elegante
Próximo: Capítulo 6: Funciones de Primera Clase
Ejercicios
- Fibonacci con Return Múltiple: Retorna el número Fibonacci y el anterior.
function fib(n)
-- Tu código aquí
end
-- >>> curr, prev = fib(7)
-- >>> print(curr, prev)
-- 13 8
- Split String: Divide una string y retorna múltiples valores.
function split_varargs(str, sep)
-- Tu código aquí
end
-- >>> a, b, c = split_varargs("hello,world,lua", ",")
-- >>> print(a, b, c)
-- hello world lua
- Merge Tables Variádico: Combina N tablas en una.
function merge(...)
-- Tu código aquí
end
-- >>> t = merge({a=1}, {b=2}, {c=3})
-- >>> print(t.a, t.b, t.c)
-- 1 2 3