← Volver al listado de tecnologías

Capítulo 6: Funciones de Primera Clase

Por: Artiko
luafunctionshigher-order-functionsfunctional-programming

Capítulo 6: Funciones de Primera Clase

“Funciones son datos, datos son funciones.” — Alan Perlis

En Lua, las funciones son valores de primera clase (first-class values). Esto significa que puedes:

Esta capacidad transforma la forma en que escribes código, permitiendo patrones funcionales elegantes y poderosos.

Funciones como Valores

En Lua, definir una función es realmente crear un valor de tipo function y asignarlo a una variable:

-- Estas dos formas son equivalentes:
function greet(name)
    return "Hello, " .. name
end

-- Es lo mismo que:
greet = function(name)
    return "Hello, " .. name
end

-- >>> print(type(greet))
-- function
-- >>> print(greet("Alice"))
-- Hello, Alice

Funciones Anónimas

Puedes crear funciones sin nombre (anónimas):

-- >>> local double = function(x) return x * 2 end
-- >>> print(double(5))
-- 10

-- Pasar función anónima directamente
-- >>> table.sort({5, 2, 8, 1}, function(a, b) return a > b end)

Almacenar Funciones en Tablas

local math_ops = {
    add = function(a, b) return a + b end,
    sub = function(a, b) return a - b end,
    mul = function(a, b) return a * b end,
    div = function(a, b) return a / b end
}

-- >>> print(math_ops.add(10, 5))
-- 15
-- >>> print(math_ops.mul(4, 3))
-- 12

Esto es la base de la orientación a objetos en Lua (más sobre esto en el Capítulo 11).

Higher-Order Functions

Una higher-order function es una función que:

  1. Acepta otras funciones como argumentos, o
  2. Retorna una función

Funciones que Aceptan Funciones

El ejemplo más simple es table.sort con un comparador custom:

local users = {
    {name = "Charlie", age = 35},
    {name = "Alice", age = 25},
    {name = "Bob", age = 30}
}

-- >>> table.sort(users, function(a, b) return a.age < b.age end)
-- >>> for _, user in ipairs(users) do
-- >>>     print(user.name, user.age)
-- >>> end
-- Alice    25
-- Bob    30
-- Charlie    35

Implementar map

La función map aplica una función a cada elemento de un array:

local function map(tbl, fn)
    local result = {}
    for i, v in ipairs(tbl) do
        result[i] = fn(v)
    end
    return result
end

-- >>> local numbers = {1, 2, 3, 4, 5}
-- >>> local doubled = map(numbers, function(x) return x * 2 end)
-- >>> print(table.concat(doubled, ", "))
-- 2, 4, 6, 8, 10

-- >>> local words = {"lua", "is", "awesome"}
-- >>> local uppercase = map(words, string.upper)
-- >>> print(table.concat(uppercase, " "))
-- LUA IS AWESOME

Implementar filter

La función filter retorna solo los elementos que cumplen una condición:

local function filter(tbl, predicate)
    local result = {}
    for _, v in ipairs(tbl) do
        if predicate(v) then
            table.insert(result, v)
        end
    end
    return result
end

-- >>> local numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
-- >>> local evens = filter(numbers, function(x) return x % 2 == 0 end)
-- >>> print(table.concat(evens, ", "))
-- 2, 4, 6, 8, 10

-- >>> local users = {
-- >>>     {name = "Alice", active = true},
-- >>>     {name = "Bob", active = false},
-- >>>     {name = "Charlie", active = true}
-- >>> }
-- >>> local active_users = filter(users, function(u) return u.active end)
-- >>> for _, u in ipairs(active_users) do print(u.name) end
-- Alice
-- Charlie

Implementar reduce (fold)

La función reduce combina todos los elementos en un solo valor:

local function reduce(tbl, fn, initial)
    local acc = initial
    for _, v in ipairs(tbl) do
        acc = fn(acc, v)
    end
    return acc
end

-- >>> local numbers = {1, 2, 3, 4, 5}
-- >>> local sum = reduce(numbers, function(acc, x) return acc + x end, 0)
-- >>> print(sum)
-- 15

-- >>> local product = reduce(numbers, function(acc, x) return acc * x end, 1)
-- >>> print(product)
-- 120

-- >>> local words = {"Lua", "is", "awesome"}
-- >>> local sentence = reduce(words, function(acc, w) return acc .. " " .. w end, "")
-- >>> print(sentence)
--  Lua is awesome

Funciones que Retornan Funciones

Aquí es donde las cosas se ponen interesantes.

Factory Functions

Crear funciones que generan otras funciones:

local function make_adder(n)
    return function(x)
        return x + n
    end
end

-- >>> local add5 = make_adder(5)
-- >>> local add10 = make_adder(10)
-- >>> print(add5(3))
-- 8
-- >>> print(add10(3))
-- 13

¿Qué está pasando aquí? make_adder retorna una closure que “recuerda” el valor de n. Hablaremos más sobre closures en el próximo capítulo.

Multiplicadores Dinámicos

local function make_multiplier(factor)
    return function(x)
        return x * factor
    end
end

-- >>> local double = make_multiplier(2)
-- >>> local triple = make_multiplier(3)
-- >>> print(double(5))
-- 10
-- >>> print(triple(5))
-- 15

Aplicación Parcial

La aplicación parcial (partial application) es fijar algunos argumentos de una función, creando una nueva función con menos parámetros:

local function partial(fn, ...)
    local args1 = {...}
    return function(...)
        local args2 = {...}
        -- Combinar args1 y args2
        local all_args = {}
        for _, v in ipairs(args1) do table.insert(all_args, v) end
        for _, v in ipairs(args2) do table.insert(all_args, v) end
        return fn(table.unpack(all_args))
    end
end

-- Ejemplo: función con 3 parámetros
local function greet(greeting, name, punctuation)
    return greeting .. ", " .. name .. punctuation
end

-- >>> local say_hello = partial(greet, "Hello")
-- >>> print(say_hello("Alice", "!"))
-- Hello, Alice!

-- >>> local greet_alice = partial(greet, "Hello", "Alice")
-- >>> print(greet_alice("!!!"))
-- Hello, Alice!!!

Aplicación Parcial con Posiciones

Versión más avanzada que permite usar placeholders:

local PLACEHOLDER = {}

local function partial_advanced(fn, ...)
    local partial_args = {...}
    return function(...)
        local call_args = {...}
        local final_args = {}
        local call_idx = 1

        for _, arg in ipairs(partial_args) do
            if arg == PLACEHOLDER then
                table.insert(final_args, call_args[call_idx])
                call_idx = call_idx + 1
            else
                table.insert(final_args, arg)
            end
        end

        -- Agregar argumentos restantes
        while call_idx <= #call_args do
            table.insert(final_args, call_args[call_idx])
            call_idx = call_idx + 1
        end

        return fn(table.unpack(final_args))
    end
end

-- >>> local divide = function(a, b) return a / b end
-- >>> local divide_by_2 = partial_advanced(divide, PLACEHOLDER, 2)
-- >>> print(divide_by_2(10))
-- 5

Composición de Funciones

Combinar funciones pequeñas para crear funciones más complejas:

local function compose(f, g)
    return function(...)
        return f(g(...))
    end
end

-- >>> local add1 = function(x) return x + 1 end
-- >>> local mul2 = function(x) return x * 2 end
-- >>> local add1_then_mul2 = compose(mul2, add1)
-- >>> print(add1_then_mul2(5))
-- 12  -- (5 + 1) * 2

-- >>> local mul2_then_add1 = compose(add1, mul2)
-- >>> print(mul2_then_add1(5))
-- 11  -- (5 * 2) + 1

Composición de N Funciones

local function compose_all(...)
    local functions = {...}
    return function(...)
        local result = {functions[1](...)}
        for i = 2, #functions do
            result = {functions[i](table.unpack(result))}
        end
        return table.unpack(result)
    end
end

-- >>> local trim = function(s) return s:match("^%s*(.-)%s*$") end
-- >>> local upper = string.upper
-- >>> local exclaim = function(s) return s .. "!!!" end
-- >>> local process = compose_all(exclaim, upper, trim)
-- >>> print(process("  hello world  "))
-- HELLO WORLD!!!

Caso Práctico: Pipeline de Datos

Procesar una lista de usuarios con una serie de transformaciones:

local function pipe(value, ...)
    local functions = {...}
    local result = value
    for _, fn in ipairs(functions) do
        result = fn(result)
    end
    return result
end

-- Funciones helper
local function filter_active(users)
    return filter(users, function(u) return u.active end)
end

local function sort_by_age(users)
    table.sort(users, function(a, b) return a.age < b.age end)
    return users
end

local function get_names(users)
    return map(users, function(u) return u.name end)
end

-- Pipeline
local users = {
    {name = "Alice", age = 30, active = true},
    {name = "Bob", age = 25, active = false},
    {name = "Charlie", age = 35, active = true},
    {name = "Diana", age = 28, active = true}
}

-- >>> local result = pipe(users,
-- >>>     filter_active,
-- >>>     sort_by_age,
-- >>>     get_names
-- >>> )
-- >>> print(table.concat(result, ", "))
-- Diana, Alice, Charlie

Caso Práctico: Validación de Datos

Sistema de validación componible:

local Validate = {}

function Validate.required(field_name)
    return function(value)
        if value == nil or value == "" then
            return false, field_name .. " is required"
        end
        return true, value
    end
end

function Validate.min_length(min, field_name)
    return function(value)
        if type(value) ~= "string" or #value < min then
            return false, field_name .. " must be at least " .. min .. " characters"
        end
        return true, value
    end
end

function Validate.max_length(max, field_name)
    return function(value)
        if type(value) == "string" and #value > max then
            return false, field_name .. " must be at most " .. max .. " characters"
        end
        return true, value
    end
end

function Validate.pattern(pattern, field_name, error_msg)
    return function(value)
        if type(value) ~= "string" or not value:match(pattern) then
            return false, error_msg or (field_name .. " is invalid")
        end
        return true, value
    end
end

function Validate.all(...)
    local validators = {...}
    return function(value)
        for _, validator in ipairs(validators) do
            local ok, err = validator(value)
            if not ok then
                return false, err
            end
        end
        return true, value
    end
end

-- Uso
local email_validator = Validate.all(
    Validate.required("Email"),
    Validate.pattern("^[%w._%%-]+@[%w._%%-]+%.%a+$", "Email", "Email must be valid")
)

local password_validator = Validate.all(
    Validate.required("Password"),
    Validate.min_length(8, "Password"),
    Validate.pattern("%d", "Password", "Password must contain at least one digit")
)

-- >>> local ok, err = email_validator("[email protected]")
-- >>> print(ok, err)
-- true    [email protected]

-- >>> ok, err = email_validator("invalid")
-- >>> print(ok, err)
-- false    Email must be valid

-- >>> ok, err = password_validator("abc")
-- >>> print(ok, err)
-- false    Password must be at least 8 characters

DEEP DIVE: Funciones vs Closures

Técnicamente, todas las funciones en Lua son closures. Una función siempre tiene acceso a:

  1. Sus parámetros locales
  2. Variables locales declaradas en su cuerpo
  3. Variables del scope exterior (upvalues)
local count = 0

local function increment()
    count = count + 1  -- 'count' es un upvalue
    return count
end

-- >>> print(increment())
-- 1
-- >>> print(increment())
-- 2
-- >>> print(increment())
-- 3

La función increment “captura” la variable count del scope exterior. Esto la convierte en un closure.

En el próximo capítulo, profundizaremos en closures y sus aplicaciones.

SOAPBOX: Lua vs JavaScript

En JavaScript, tienes function y arrow functions (=>). En Lua, solo tienes function. Pero las funciones de Lua son más poderosas porque:

  1. Siempre son closures (capturan upvalues automáticamente)
  2. Soportan múltiples valores de retorno
  3. Pueden tener varargs (...)

Todo esto sin sintaxis especial ni “funciones flecha”.

Patrones de Performance

Evitar Crear Funciones en Loops

-- ❌ MALO: Crea 10,000 funciones
local handlers = {}
for i = 1, 10000 do
    handlers[i] = function() print("Handler " .. i) end
end

-- ✅ MEJOR: Reutilizar una función factory
local function make_handler(id)
    return function() print("Handler " .. id) end
end

local handlers = {}
for i = 1, 10000 do
    handlers[i] = make_handler(i)
end

Memoización Simple

local function memoize(fn)
    local cache = {}
    return function(...)
        local key = table.concat({...}, ",")
        if cache[key] == nil then
            cache[key] = fn(...)
        end
        return cache[key]
    end
end

-- Fibonacci sin memoización: exponencial O(2^n)
local function fib_slow(n)
    if n <= 1 then return n end
    return fib_slow(n - 1) + fib_slow(n - 2)
end

-- Fibonacci con memoización: lineal O(n)
local fib_fast = memoize(function(n)
    if n <= 1 then return n end
    return fib_fast(n - 1) + fib_fast(n - 2)
end)

-- >>> print(fib_fast(40))  -- Instantáneo
-- 102334155

Resumen del Capítulo

Las funciones de primera clase en Lua permiten:

Próximo: Capítulo 7: Closures - Fábricas de Funciones


Ejercicios

  1. Implementar find: Función que retorna el primer elemento que cumple una condición.
function find(tbl, predicate)
    -- Tu código aquí
end

-- >>> local users = {{name="Alice", age=25}, {name="Bob", age=30}}
-- >>> local user = find(users, function(u) return u.age > 28 end)
-- >>> print(user.name)
-- Bob
  1. Curry: Convierte una función de N argumentos en N funciones de 1 argumento.
function curry(fn, arity)
    -- Tu código aquí
end

-- >>> local add3 = function(a, b, c) return a + b + c end
-- >>> local curried = curry(add3, 3)
-- >>> print(curried(1)(2)(3))
-- 6
  1. Implementar every y some: Verificar si todos/algunos elementos cumplen una condición.
function every(tbl, predicate)
    -- Tu código aquí
end

function some(tbl, predicate)
    -- Tu código aquí
end

-- >>> local numbers = {2, 4, 6, 8}
-- >>> print(every(numbers, function(x) return x % 2 == 0 end))
-- true
-- >>> print(some(numbers, function(x) return x > 5 end))
-- true

Lecturas Adicionales