Capítulo 7: Closures - Fábricas de Funciones
Capítulo 7: Closures - Fábricas de Funciones
“Un closure es una función con memoria.” — Anónimo
En el capítulo anterior vimos funciones de primera clase. Ahora profundizaremos en una de sus aplicaciones más poderosas: closures.
Un closure es una función que captura variables del scope exterior. Estas variables se llaman upvalues, y permiten que la función “recuerde” su entorno incluso después de que el scope original haya terminado.
Entendiendo Upvalues
local function make_counter()
local count = 0 -- upvalue
return function()
count = count + 1
return count
end
end
-- >>> local counter1 = make_counter()
-- >>> local counter2 = make_counter()
-- >>> print(counter1())
-- 1
-- >>> print(counter1())
-- 2
-- >>> print(counter2())
-- 1
-- >>> print(counter1())
-- 3
¿Qué está pasando aquí?
make_countercrea una variable localcount = 0- Retorna una función anónima que captura
count - Cada vez que llamas a
counter1(), accede a SU PROPIA copia decount counter2()tiene una copia DIFERENTE decount
La variable count vive mientras exista la función que la capturó. Esto es un closure.
Múltiples Closures Compartiendo Estado
Puedes retornar múltiples funciones que compartan el mismo upvalue:
local function make_account(initial_balance)
local balance = initial_balance -- upvalue compartido
local function deposit(amount)
balance = balance + amount
return balance
end
local function withdraw(amount)
if balance >= amount then
balance = balance - amount
return balance
else
return nil, "Insufficient funds"
end
end
local function get_balance()
return balance
end
return {
deposit = deposit,
withdraw = withdraw,
get_balance = get_balance
}
end
-- >>> local account = make_account(100)
-- >>> print(account.get_balance())
-- 100
-- >>> print(account.deposit(50))
-- 150
-- >>> print(account.withdraw(30))
-- 120
-- >>> local ok, err = account.withdraw(200)
-- >>> print(ok, err)
-- nil Insufficient funds
Las tres funciones (deposit, withdraw, get_balance) comparten el MISMO balance. Esto es encapsulación sin clases.
NOTA: Variables Privadas
La variable
balancees completamente privada. No hay forma de accederla directamente desde fuera. Solo puedes interactuar con ella a través de los métodos públicos. Esto es verdadera encapsulación.
El Patrón Módulo Privado
Los closures permiten crear módulos con estado privado:
local function create_logger(prefix)
local log_count = 0
local enabled = true
local function log(message)
if not enabled then return end
log_count = log_count + 1
print(string.format("[%s] #%d: %s", prefix, log_count, message))
end
local function enable()
enabled = true
end
local function disable()
enabled = false
end
local function get_count()
return log_count
end
return {
log = log,
enable = enable,
disable = disable,
get_count = get_count
}
end
-- >>> local logger = create_logger("APP")
-- >>> logger.log("Server started")
-- [APP] #1: Server started
-- >>> logger.log("Request received")
-- [APP] #2: Request received
-- >>> logger.disable()
-- >>> logger.log("This won't print")
-- >>> logger.enable()
-- >>> logger.log("Back online")
-- [APP] #3: Back online
-- >>> print("Total logs:", logger.get_count())
-- Total logs: 3
Factory Functions Avanzadas
Crear Validadores Configurables
local function make_range_validator(min, max)
return function(value)
if value < min then
return false, string.format("Value must be >= %d", min)
end
if value > max then
return false, string.format("Value must be <= %d", max)
end
return true, value
end
end
local function make_length_validator(min, max)
return function(value)
local len = #value
if len < min then
return false, string.format("Length must be >= %d", min)
end
if max and len > max then
return false, string.format("Length must be <= %d", max)
end
return true, value
end
end
-- >>> local age_validator = make_range_validator(18, 100)
-- >>> local ok, err = age_validator(15)
-- >>> print(ok, err)
-- false Value must be >= 18
-- >>> local username_validator = make_length_validator(3, 20)
-- >>> ok, err = username_validator("ab")
-- >>> print(ok, err)
-- false Length must be >= 3
Crear Formateadores
local function make_formatter(template)
return function(data)
return (template:gsub("{(%w+)}", function(key)
return tostring(data[key] or "")
end))
end
end
-- >>> local user_formatter = make_formatter("Name: {name}, Age: {age}")
-- >>> print(user_formatter({name = "Alice", age = 30}))
-- Name: Alice, Age: 30
-- >>> local log_formatter = make_formatter("[{level}] {message}")
-- >>> print(log_formatter({level = "ERROR", message = "File not found"}))
-- [ERROR] File not found
Iteradores Personalizados con Closures
Los iteradores en Lua se implementan con closures. Veamos cómo:
Iterador Básico
local function range(from, to, step)
step = step or 1
local current = from - step
return function()
current = current + step
if current <= to then
return current
end
end
end
-- >>> for i in range(1, 5) do
-- >>> print(i)
-- >>> end
-- 1
-- 2
-- 3
-- 4
-- 5
-- >>> for i in range(0, 10, 2) do
-- >>> print(i)
-- >>> end
-- 0
-- 2
-- 4
-- 6
-- 8
-- 10
Iterador de Líneas Custom
local function lines_iterator(text)
local pos = 1
return function()
if pos > #text then return nil end
local line_end = text:find("\n", pos) or #text + 1
local line = text:sub(pos, line_end - 1)
pos = line_end + 1
return line
end
end
-- >>> local text = "Hello\nWorld\nLua"
-- >>> for line in lines_iterator(text) do
-- >>> print(line)
-- >>> end
-- Hello
-- World
-- Lua
Iterador con Estado Múltiple
local function enumerate(tbl)
local index = 0
return function()
index = index + 1
if index <= #tbl then
return index, tbl[index]
end
end
end
-- >>> local fruits = {"apple", "banana", "cherry"}
-- >>> for i, fruit in enumerate(fruits) do
-- >>> print(i, fruit)
-- >>> end
-- 1 apple
-- 2 banana
-- 3 cherry
Iterador Infinito
local function fibonacci()
local a, b = 0, 1
return function()
a, b = b, a + b
return a
end
end
-- >>> local fib = fibonacci()
-- >>> for i = 1, 10 do
-- >>> print(fib())
-- >>> end
-- 1
-- 1
-- 2
-- 3
-- 5
-- 8
-- 13
-- 21
-- 34
-- 55
Caso Práctico: Rate Limiter
Implementar un rate limiter usando closures:
local function make_rate_limiter(max_calls, time_window)
local calls = {}
return function()
local now = os.time()
-- Eliminar llamadas fuera de la ventana de tiempo
local new_calls = {}
for _, timestamp in ipairs(calls) do
if now - timestamp < time_window then
table.insert(new_calls, timestamp)
end
end
calls = new_calls
-- Verificar límite
if #calls >= max_calls then
return false, "Rate limit exceeded"
end
-- Registrar nueva llamada
table.insert(calls, now)
return true
end
end
-- Permitir máximo 5 llamadas por segundo
local api_limiter = make_rate_limiter(5, 1)
-- >>> for i = 1, 7 do
-- >>> local ok, err = api_limiter()
-- >>> if ok then
-- >>> print("Request " .. i .. " OK")
-- >>> else
-- >>> print("Request " .. i .. " " .. err)
-- >>> end
-- >>> end
-- Request 1 OK
-- Request 2 OK
-- Request 3 OK
-- Request 4 OK
-- Request 5 OK
-- Request 6 Rate limit exceeded
-- Request 7 Rate limit exceeded
Caso Práctico: Caché con TTL
Cache con tiempo de expiración:
local function make_cache(ttl)
local cache = {}
local function get(key)
local entry = cache[key]
if not entry then return nil end
local now = os.time()
if now - entry.timestamp > ttl then
cache[key] = nil -- Expirado
return nil
end
return entry.value
end
local function set(key, value)
cache[key] = {
value = value,
timestamp = os.time()
}
end
local function clear()
cache = {}
end
local function size()
local count = 0
for _ in pairs(cache) do count = count + 1 end
return count
end
return {
get = get,
set = set,
clear = clear,
size = size
}
end
-- >>> local cache = make_cache(5) -- TTL de 5 segundos
-- >>> cache.set("user:1", {name = "Alice", age = 30})
-- >>> print(cache.get("user:1").name)
-- Alice
-- >>> -- Después de 6 segundos...
-- >>> os.execute("sleep 6")
-- >>> print(cache.get("user:1"))
-- nil
Caso Práctico: Event Emitter
Sistema de eventos con closures:
local function create_event_emitter()
local listeners = {}
local function on(event, callback)
if not listeners[event] then
listeners[event] = {}
end
table.insert(listeners[event], callback)
end
local function off(event, callback)
if not listeners[event] then return end
for i, cb in ipairs(listeners[event]) do
if cb == callback then
table.remove(listeners[event], i)
break
end
end
end
local function emit(event, ...)
if not listeners[event] then return end
for _, callback in ipairs(listeners[event]) do
callback(...)
end
end
local function once(event, callback)
local function wrapper(...)
callback(...)
off(event, wrapper)
end
on(event, wrapper)
end
return {
on = on,
off = off,
emit = emit,
once = once
}
end
-- >>> local emitter = create_event_emitter()
-- >>> emitter.on("data", function(data)
-- >>> print("Received:", data)
-- >>> end)
-- >>> emitter.once("error", function(err)
-- >>> print("Error (once):", err)
-- >>> end)
-- >>> emitter.emit("data", "Hello")
-- Received: Hello
-- >>> emitter.emit("data", "World")
-- Received: World
-- >>> emitter.emit("error", "Something went wrong")
-- Error (once): Something went wrong
-- >>> emitter.emit("error", "Another error")
-- (no output - listener was removed after first call)
DEEP DIVE: Lifetime de Upvalues
¿Cuánto vive un upvalue? Mientras exista alguna función que lo capture.
local function demo()
local x = 10
local function inner()
print(x)
end
return inner
end
-- >>> local fn = demo()
-- La función demo() ya terminó, pero x sigue viva
-- >>> fn()
-- 10
Internamente, Lua mueve x del stack al heap cuando detecta que inner la captura. Esto garantiza que x sobreviva después de que demo() retorne.
Múltiples Closures
local function factory()
local shared = 0
local function increment()
shared = shared + 1
return shared
end
local function decrement()
shared = shared - 1
return shared
end
return increment, decrement
end
-- >>> local inc, dec = factory()
-- >>> print(inc())
-- 1
-- >>> print(inc())
-- 2
-- >>> print(dec())
-- 1
Ambas funciones comparten el MISMO shared (mismo upvalue).
Closures vs Tablas
¿Cuándo usar closures y cuándo usar tablas?
Usa closures cuando:
- Necesitas encapsulación verdadera (variables privadas)
- El estado es pequeño y simple
- Quieres generar múltiples instancias independientes
Usa tablas cuando:
- Necesitas inspeccionar/serializar el estado
- El estado es complejo (muchos campos)
- Quieres usar herencia u OOP tradicional
-- Closure: Estado privado
local function make_counter()
local count = 0
return function() count = count + 1; return count end
end
-- Tabla: Estado visible
local Counter = {}
function Counter.new()
return {count = 0}
end
function Counter:increment()
self.count = self.count + 1
return self.count
end
SOAPBOX: Closures vs Clases
En lenguajes como Java, necesitas una clase completa para encapsular estado:
class Counter { private int count = 0; public int increment() { return ++count; } }En Lua, un simple closure hace lo mismo con menos código y sin ceremonias.
Resumen del Capítulo
Closures en Lua:
- Capturan upvalues: Variables del scope exterior
- Viven más allá del scope: Los upvalues sobreviven mientras exista el closure
- Encapsulación: Variables verdaderamente privadas
- Factory functions: Generar funciones configurables
- Iteradores: Base de los iteradores personalizados
- Estado compartido: Múltiples funciones pueden compartir upvalues
Próximo: Capítulo 8: Decoradores al Estilo Lua
Ejercicios
- Implementar
debounce: Función que ejecuta después de N segundos sin llamadas.
function debounce(fn, delay)
-- Tu código aquí
end
-- >>> local save = debounce(function() print("Saved!") end, 1)
-- >>> save() -- No imprime
-- >>> save() -- No imprime
-- >>> -- Esperar 1 segundo
-- >>> -- Imprime "Saved!" solo una vez
- Crear un Generador de IDs: Función que retorna IDs únicos incrementales.
function make_id_generator(prefix)
-- Tu código aquí
end
-- >>> local user_ids = make_id_generator("user_")
-- >>> print(user_ids())
-- user_1
-- >>> print(user_ids())
-- user_2
- Implementar
memoizecon TTL: Caché con expiración.
function memoize_with_ttl(fn, ttl)
-- Tu código aquí
end
-- >>> local expensive = memoize_with_ttl(function(x) return x * x end, 5)
-- >>> print(expensive(5)) -- Calcula
-- 25
-- >>> print(expensive(5)) -- Usa caché
-- 25
-- >>> -- Después de 5 segundos, vuelve a calcular
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 27: Closures
- Lua 5.4 Reference Manual - Section 3.5: Visibility Rules
- Closures in Lua