← Volver al listado de tecnologías

Capítulo 5: Múltiples Valores y Destructuring

Por: Artiko
luamultiple-returnvarargsdestructuringidioms

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: #{...} vs select('#', ...)

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:

Próximo: Capítulo 6: Funciones de Primera Clase


Ejercicios

  1. 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
  1. 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
  1. 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