← Volver al listado de tecnologías

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

Por: Artiko
luaclosuresupvaluesencapsulationiterators

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í?

  1. make_counter crea una variable local count = 0
  2. Retorna una función anónima que captura count
  3. Cada vez que llamas a counter1(), accede a SU PROPIA copia de count
  4. counter2() tiene una copia DIFERENTE de count

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 balance es 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:

Usa tablas cuando:

-- 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:

Próximo: Capítulo 8: Decoradores al Estilo Lua


Ejercicios

  1. 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
  1. 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
  1. Implementar memoize con 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