Capítulo 8: Decoradores al Estilo Lua
Capítulo 8: Decoradores al Estilo Lua
“El código es como el humor. Cuando tienes que explicarlo, es malo.” — Cory House
En Python, los decoradores son una característica del lenguaje con sintaxis especial (@decorator). En Lua, no existe esa sintaxis, pero el concepto es aún más poderoso porque las funciones son valores de primera clase.
Un decorador es una función que toma otra función y retorna una versión “mejorada” de ella, típicamente agregando comportamiento antes o después de la ejecución original.
El Patrón Básico de Decorador
local function my_decorator(fn)
return function(...)
-- Hacer algo ANTES
local result = {fn(...)} -- Llamar función original
-- Hacer algo DESPUÉS
return table.unpack(result)
end
end
Este patrón preserva:
- Los argumentos (usando
...) - Los valores de retorno (usando
table.unpack) - La capacidad de múltiples returns
Decorador de Logging
Loggear automáticamente cada llamada a función:
local function with_logging(fn, name)
name = name or "function"
return function(...)
local args = {...}
local args_str = table.concat(vim.tbl_map(tostring, args), ", ")
print(string.format("[LOG] Calling %s(%s)", name, args_str))
local results = {fn(...)}
if #results > 0 then
local results_str = table.concat(vim.tbl_map(tostring, results), ", ")
print(string.format("[LOG] %s returned: %s", name, results_str))
end
return table.unpack(results)
end
end
-- Función original
local function add(a, b)
return a + b
end
-- Decorar
add = with_logging(add, "add")
-- >>> print(add(5, 3))
-- [LOG] Calling add(5, 3)
-- [LOG] add returned: 8
-- 8
Versión Mejorada con Niveles
local LOG_LEVEL = {
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4
}
local current_level = LOG_LEVEL.INFO
local function with_logging_level(fn, name, level)
level = level or LOG_LEVEL.DEBUG
name = name or "function"
return function(...)
if level < current_level then
return fn(...) -- No loggear
end
local args = {...}
local args_str = {}
for _, v in ipairs(args) do
table.insert(args_str, tostring(v))
end
print(string.format("[%s] %s(%s)",
level >= LOG_LEVEL.ERROR and "ERROR" or
level >= LOG_LEVEL.WARN and "WARN" or
level >= LOG_LEVEL.INFO and "INFO" or "DEBUG",
name,
table.concat(args_str, ", ")))
return fn(...)
end
end
Decorador de Timing
Medir cuánto tiempo tarda una función:
local function with_timing(fn, name)
name = name or "function"
return function(...)
local start = os.clock()
local results = {fn(...)}
local elapsed = os.clock() - start
print(string.format("[TIMING] %s took %.6f seconds", name, elapsed))
return table.unpack(results)
end
end
-- >>> local function slow_operation()
-- >>> local sum = 0
-- >>> for i = 1, 1000000 do
-- >>> sum = sum + i
-- >>> end
-- >>> return sum
-- >>> end
-- >>> slow_operation = with_timing(slow_operation, "slow_operation")
-- >>> slow_operation()
-- [TIMING] slow_operation took 0.042315 seconds
Timing con Estadísticas
local function with_stats(fn, name)
name = name or "function"
local stats = {
calls = 0,
total_time = 0,
min_time = math.huge,
max_time = 0
}
local decorated = function(...)
local start = os.clock()
local results = {fn(...)}
local elapsed = os.clock() - start
stats.calls = stats.calls + 1
stats.total_time = stats.total_time + elapsed
stats.min_time = math.min(stats.min_time, elapsed)
stats.max_time = math.max(stats.max_time, elapsed)
return table.unpack(results)
end
decorated.get_stats = function()
return {
calls = stats.calls,
total_time = stats.total_time,
avg_time = stats.calls > 0 and (stats.total_time / stats.calls) or 0,
min_time = stats.min_time,
max_time = stats.max_time
}
end
decorated.print_stats = function()
local s = decorated.get_stats()
print(string.format([[
%s Statistics:
Calls: %d
Total time: %.6fs
Avg time: %.6fs
Min time: %.6fs
Max time: %.6fs
]], name, s.calls, s.total_time, s.avg_time, s.min_time, s.max_time))
end
return decorated
end
Decorador de Memoización
Cachear resultados automáticamente:
local function memoize(fn)
local cache = {}
return function(...)
-- Crear key del cache
local key = table.concat({...}, "\0")
if cache[key] == nil then
cache[key] = {fn(...)}
end
return table.unpack(cache[key])
end
end
-- Fibonacci sin memoización: O(2^n)
local function fib(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end
-- Decorar
fib = memoize(fib)
-- >>> print(fib(100)) -- Instantáneo gracias al cache
Memoización con Límite de Tamaño (LRU Cache)
local function memoize_lru(fn, max_size)
max_size = max_size or 100
local cache = {}
local keys = {} -- Orden de inserción
return function(...)
local key = table.concat({...}, "\0")
-- Si está en cache, mover al final (más reciente)
if cache[key] then
for i, k in ipairs(keys) do
if k == key then
table.remove(keys, i)
break
end
end
table.insert(keys, key)
return table.unpack(cache[key])
end
-- Calcular resultado
local results = {fn(...)}
-- Agregar al cache
cache[key] = results
table.insert(keys, key)
-- Si excede tamaño, eliminar el más antiguo
if #keys > max_size then
local oldest = table.remove(keys, 1)
cache[oldest] = nil
end
return table.unpack(results)
end
end
Decorador de Retry
Reintentar automáticamente en caso de fallo:
local function with_retry(fn, max_attempts, delay)
max_attempts = max_attempts or 3
delay = delay or 1
return function(...)
local attempts = 0
local last_error
while attempts < max_attempts do
attempts = attempts + 1
local ok, result = pcall(fn, ...)
if ok then
return result
else
last_error = result
print(string.format("[RETRY] Attempt %d/%d failed: %s",
attempts, max_attempts, result))
if attempts < max_attempts then
os.execute("sleep " .. delay)
end
end
end
error(string.format("Failed after %d attempts: %s",
max_attempts, last_error))
end
end
-- >>> local function flaky_api()
-- >>> if math.random() > 0.7 then
-- >>> return "Success!"
-- >>> else
-- >>> error("Network timeout")
-- >>> end
-- >>> end
-- >>> flaky_api = with_retry(flaky_api, 5, 0.5)
-- >>> print(flaky_api())
-- [RETRY] Attempt 1/5 failed: Network timeout
-- [RETRY] Attempt 2/5 failed: Network timeout
-- Success!
Decorador de Validación
Validar argumentos automáticamente:
local function with_validation(fn, ...)
local validators = {...}
return function(...)
local args = {...}
-- Validar cada argumento
for i, validator in ipairs(validators) do
if args[i] ~= nil then
local ok, err = validator(args[i])
if not ok then
error(string.format("Argument %d: %s", i, err))
end
end
end
return fn(...)
end
end
-- Validadores
local function is_number(x)
if type(x) ~= "number" then
return false, "must be a number"
end
return true
end
local function is_positive(x)
if type(x) == "number" and x <= 0 then
return false, "must be positive"
end
return true
end
local function is_string(x)
if type(x) ~= "string" then
return false, "must be a string"
end
return true
end
-- >>> local function greet(name, age)
-- >>> return string.format("Hello %s, you are %d years old", name, age)
-- >>> end
-- >>> greet = with_validation(greet, is_string, is_number)
-- >>> print(greet("Alice", 30))
-- Hello Alice, you are 30 years old
-- >>> greet("Alice", "thirty")
-- Error: Argument 2: must be a number
Decorador de Rate Limiting
local function with_rate_limit(fn, max_calls, window_seconds)
local calls = {}
return function(...)
local now = os.time()
-- Limpiar llamadas antiguas
local new_calls = {}
for _, timestamp in ipairs(calls) do
if now - timestamp < window_seconds then
table.insert(new_calls, timestamp)
end
end
calls = new_calls
-- Verificar límite
if #calls >= max_calls then
error(string.format("Rate limit exceeded: max %d calls per %d seconds",
max_calls, window_seconds))
end
-- Registrar llamada
table.insert(calls, now)
return fn(...)
end
end
-- >>> local function send_email(to, subject)
-- >>> print("Sending email to " .. to)
-- >>> end
-- >>> send_email = with_rate_limit(send_email, 5, 60) -- 5 emails por minuto
Composición de Decoradores
Puedes aplicar múltiples decoradores:
local function compose_decorators(...)
local decorators = {...}
return function(fn)
local result = fn
-- Aplicar decoradores de derecha a izquierda
for i = #decorators, 1, -1 do
result = decorators[i](result)
end
return result
end
end
-- >>> local function calculate(a, b)
-- >>> return a + b
-- >>> end
-- >>> calculate = compose_decorators(
-- >>> with_logging,
-- >>> with_timing,
-- >>> memoize
-- >>> )(calculate)
Helper para Aplicar Decoradores
local function decorate(fn, ...)
local result = fn
for _, decorator in ipairs({...}) do
result = decorator(result)
end
return result
end
-- >>> calculate = decorate(calculate,
-- >>> memoize,
-- >>> with_timing,
-- >>> with_logging
-- >>> )
Caso Práctico: Sistema de Permisos
local function require_permission(permission)
return function(fn)
return function(user, ...)
if not user.permissions[permission] then
error("Permission denied: " .. permission)
end
return fn(user, ...)
end
end
end
local function require_role(role)
return function(fn)
return function(user, ...)
if user.role ~= role then
error("Requires role: " .. role)
end
return fn(user, ...)
end
end
end
-- >>> local function delete_user(user, user_id)
-- >>> print("Deleting user " .. user_id)
-- >>> end
-- >>> delete_user = require_permission("users.delete")(delete_user)
-- >>> delete_user = require_role("admin")(delete_user)
-- >>> local admin = {role = "admin", permissions = {["users.delete"] = true}}
-- >>> delete_user(admin, 123)
-- Deleting user 123
-- >>> local regular_user = {role = "user", permissions = {}}
-- >>> delete_user(regular_user, 123)
-- Error: Requires role: admin
Caso Práctico: Lazy Evaluation
Ejecutar función solo cuando se necesita el resultado:
local function lazy(fn)
local cached = nil
local computed = false
return function(...)
if not computed then
cached = {fn(...)}
computed = true
end
return table.unpack(cached)
end
end
-- >>> local function expensive_calculation()
-- >>> print("Computing...")
-- >>> local sum = 0
-- >>> for i = 1, 1000000 do
-- >>> sum = sum + i
-- >>> end
-- >>> return sum
-- >>> end
-- >>> expensive_calculation = lazy(expensive_calculation)
-- >>> -- No imprime "Computing..." todavía
-- >>> print("Result:", expensive_calculation())
-- Computing...
-- Result: 500000500000
-- >>> print("Result:", expensive_calculation())
-- Result: 500000500000
-- (no vuelve a calcular)
DEEP DIVE: Preservar Metadata
Cuando decoras una función, pierdes metadata como su nombre. Podemos preservarla:
local function preserve_metadata(original, decorated)
-- En Lua no hay mucha metadata nativa, pero podemos agregar nuestra propia
if debug and debug.getinfo then
local info = debug.getinfo(original, "S")
decorated._source = info.source
decorated._linedefined = info.linedefined
end
decorated._original = original
return decorated
end
local function smart_decorator(fn)
local decorated = function(...)
-- ... lógica del decorador
return fn(...)
end
return preserve_metadata(fn, decorated)
end
Decoradores para Clases
Aunque Lua no tiene clases nativas, podemos decorar métodos:
local function decorate_methods(class, decorator)
local decorated_class = {}
for key, value in pairs(class) do
if type(value) == "function" then
decorated_class[key] = decorator(value, key)
else
decorated_class[key] = value
end
end
return setmetatable(decorated_class, getmetatable(class))
end
-- >>> local MyClass = {
-- >>> add = function(self, a, b) return a + b end,
-- >>> sub = function(self, a, b) return a - b end
-- >>> }
-- >>> MyClass = decorate_methods(MyClass, with_logging)
SOAPBOX: Decoradores vs Aspectos
Los decoradores son una forma de Aspect-Oriented Programming (AOP). En lenguajes como Java, necesitas frameworks como AspectJ. En Lua, es built-in gracias a funciones de primera clase.
Resumen del Capítulo
Decoradores en Lua:
- Patrón básico: Función que toma función y retorna función mejorada
- Preservar argumentos/returns: Usar
...ytable.unpack - Casos de uso comunes: Logging, timing, memoización, retry, validación
- Composición: Aplicar múltiples decoradores
- Aspect-Oriented Programming: Separar concerns cross-cutting
Próximo: Capítulo 9: Introspección y Atributos
Ejercicios
- Implementar
once: Decorador que permite ejecutar función solo una vez.
function once(fn)
-- Tu código aquí
end
-- >>> local initialize = once(function() print("Initialized!") end)
-- >>> initialize()
-- Initialized!
-- >>> initialize()
-- (no imprime nada)
- Deprecation Decorator: Avisar que una función está deprecada.
function deprecated(fn, message)
-- Tu código aquí
end
-- >>> old_function = deprecated(old_function, "Use new_function instead")
-- >>> old_function()
-- [WARNING] This function is deprecated: Use new_function instead
- Timeout Decorator: Abortar función si tarda más de N segundos.
function with_timeout(fn, timeout_seconds)
-- Tu código aquí (pista: usa coroutines)
end
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 9: Closures
- Decorators in Lua
- Aspect-Oriented Programming