Capítulo 6: Funciones de Primera Clase
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:
- Asignar funciones a variables
- Pasar funciones como argumentos
- Retornar funciones desde otras funciones
- Almacenar funciones en tablas
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:
- Acepta otras funciones como argumentos, o
- 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:
- Sus parámetros locales
- Variables locales declaradas en su cuerpo
- 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
functiony arrow functions (=>). En Lua, solo tienesfunction. Pero las funciones de Lua son más poderosas porque:
- Siempre son closures (capturan upvalues automáticamente)
- Soportan múltiples valores de retorno
- 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:
- Asignar funciones a variables y almacenarlas en tablas
- Pasar funciones como argumentos (higher-order functions)
- Retornar funciones desde otras funciones
- Implementar patrones funcionales: map, filter, reduce
- Aplicación parcial: Fijar argumentos para crear funciones especializadas
- Composición: Combinar funciones pequeñas en funciones complejas
- Pipelines: Procesar datos a través de una serie de transformaciones
Próximo: Capítulo 7: Closures - Fábricas de Funciones
Ejercicios
- 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
- 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
- Implementar
everyysome: 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
- Programming in Lua, 4th edition - Capítulo 6: Functions
- Lua 5.4 Reference Manual - Section 3.4.11: Function Definitions
- Functional Programming in Lua