Capítulo 13: Protocolos y Duck Typing
Capítulo 13: Protocolos y Duck Typing
1. ¿Qué es un Protocolo?
Un protocolo es un conjunto de metamétodos o funciones que un objeto debe implementar para cumplir con cierto comportamiento esperado. A diferencia de las interfaces formales en lenguajes como Java o TypeScript, Lua utiliza duck typing: “Si camina como un pato y grazna como un pato, entonces es un pato”.
Concepto Fundamental
En Lua, un protocolo es una convención implícita sobre qué métodos o metamétodos debe tener un objeto para ser usado de cierta manera:
-- Protocolo "Iterable": debe tener __pairs o __ipairs
-- Protocolo "Callable": debe tener __call
-- Protocolo "Stringable": debe tener __tostring
-- Protocolo "Comparable": debe tener __eq, __lt, etc.
Ejemplo Básico
-- Un objeto que cumple el protocolo "Stringable"
local Person = {}
Person.__index = Person
function Person.new(name, age)
local self = setmetatable({}, Person)
self.name = name
self.age = age
return self
end
-- Implementamos el protocolo __tostring
function Person:__tostring()
return string.format("%s (%d años)", self.name, self.age)
end
local p = Person.new("Ana", 30)
print(p) -- Ana (30 años)
Ventajas del Duck Typing
-- No necesitamos declarar que Person "implementa" Stringable
-- Solo necesitamos que tenga el método correcto
local function describe(obj)
-- Si tiene __tostring, lo usamos
if getmetatable(obj) and getmetatable(obj).__tostring then
return tostring(obj)
end
return "Objeto sin descripción"
end
print(describe(p)) -- Ana (30 años)
print(describe({})) -- Objeto sin descripción
2. Protocolo Iterable: __pairs, __ipairs
El protocolo Iterable permite que un objeto sea recorrido con pairs() o ipairs(). Lua 5.2+ permite sobrescribir estos comportamientos mediante metamétodos.
__pairs: Iteración Genérica
local Set = {}
Set.__index = Set
function Set.new(elements)
local self = setmetatable({}, Set)
self.elements = {}
for _, v in ipairs(elements or {}) do
self.elements[v] = true
end
return self
end
function Set:add(value)
self.elements[value] = true
end
function Set:contains(value)
return self.elements[value] == true
end
-- Implementar protocolo Iterable con __pairs
function Set:__pairs()
local keys = {}
for k in pairs(self.elements) do
table.insert(keys, k)
end
table.sort(keys)
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], true
end
end
end
-- Uso
local s = Set.new({3, 1, 4, 1, 5, 9})
s:add(2)
for value in pairs(s) do
print(value) -- 1, 2, 3, 4, 5, 9 (ordenados)
end
__ipairs: Iteración Secuencial
local Range = {}
Range.__index = Range
function Range.new(start, stop, step)
local self = setmetatable({}, Range)
self.start = start
self.stop = stop
self.step = step or 1
return self
end
-- Implementar __ipairs para iteración secuencial
function Range:__ipairs()
local current = self.start - self.step
return function()
current = current + self.step
if (self.step > 0 and current <= self.stop) or
(self.step < 0 and current >= self.stop) then
return current, current
end
end
end
-- Uso
local r = Range.new(1, 10, 2)
for i, v in ipairs(r) do
print(i, v) -- 1, 3, 5, 7, 9
end
Iterable Personalizado con Estado
local Fibonacci = {}
Fibonacci.__index = Fibonacci
function Fibonacci.new(max_terms)
local self = setmetatable({}, Fibonacci)
self.max = max_terms or 10
return self
end
function Fibonacci:__pairs()
local a, b = 0, 1
local count = 0
local max = self.max
return function()
if count >= max then return nil end
count = count + 1
a, b = b, a + b
return count, a
end
end
-- Uso
local fib = Fibonacci.new(8)
for i, value in pairs(fib) do
print(string.format("fib(%d) = %d", i, value))
end
-- fib(1) = 1, fib(2) = 1, fib(3) = 2, fib(4) = 3, fib(5) = 5, ...
3. Protocolo Callable: __call
El metamétodo __call convierte una tabla en un objeto callable (invocable como función).
Callable Básico
local Counter = {}
Counter.__index = Counter
function Counter.new(initial)
local self = setmetatable({}, Counter)
self.count = initial or 0
return self
end
-- Hacer el objeto callable
function Counter:__call(increment)
self.count = self.count + (increment or 1)
return self.count
end
-- Uso
local c = Counter.new(10)
print(c()) -- 11 (incrementa 1)
print(c(5)) -- 16 (incrementa 5)
print(c()) -- 17
Factory Pattern con __call
local Person = {}
Person.__index = Person
function Person:greet()
return "Hola, soy " .. self.name
end
-- La tabla Person misma es callable para crear instancias
setmetatable(Person, {
__call = function(cls, name, age)
local self = setmetatable({}, cls)
self.name = name
self.age = age
return self
end
})
-- Uso: Person() en lugar de Person.new()
local p1 = Person("Carlos", 25)
local p2 = Person("Laura", 30)
print(p1:greet()) -- Hola, soy Carlos
Memoización con Callable
local function memoize(fn)
local cache = {}
return setmetatable({}, {
__call = function(self, ...)
local key = table.concat({...}, ",")
if not cache[key] then
cache[key] = fn(...)
end
return cache[key]
end
})
end
-- Función costosa
local function fibonacci(n)
if n <= 1 then return n end
return fibonacci(n-1) + fibonacci(n-2)
end
local fib = memoize(fibonacci)
print(fib(35)) -- Calculado
print(fib(35)) -- Desde caché (instantáneo)
Composición de Funciones
local Pipe = {}
function Pipe.new(...)
local functions = {...}
return setmetatable({functions = functions}, {
__call = function(self, value)
local result = value
for _, fn in ipairs(self.functions) do
result = fn(result)
end
return result
end
})
end
-- Uso
local process = Pipe.new(
function(x) return x * 2 end,
function(x) return x + 10 end,
function(x) return x ^ 2 end
)
print(process(5)) -- ((5*2)+10)^2 = 400
4. Protocolo Stringable: __tostring
El metamétodo __tostring define cómo se convierte un objeto a string (usado por tostring() y print()).
Stringable Básico
local Point = {}
Point.__index = Point
function Point.new(x, y)
local self = setmetatable({}, Point)
self.x = x
self.y = y
return self
end
function Point:__tostring()
return string.format("Point(%d, %d)", self.x, self.y)
end
local p = Point.new(3, 4)
print(p) -- Point(3, 4)
print(tostring(p)) -- Point(3, 4)
Representación JSON-like
local function table_to_string(t, indent)
indent = indent or 0
local spaces = string.rep(" ", indent)
local lines = {}
table.insert(lines, "{")
for k, v in pairs(t) do
local key = type(k) == "string" and k or "[" .. k .. "]"
local value
if type(v) == "table" then
value = table_to_string(v, indent + 1)
elseif type(v) == "string" then
value = '"' .. v .. '"'
else
value = tostring(v)
end
table.insert(lines, spaces .. " " .. key .. ": " .. value .. ",")
end
table.insert(lines, spaces .. "}")
return table.concat(lines, "\n")
end
local User = {}
User.__index = User
function User.new(data)
return setmetatable(data, User)
end
function User:__tostring()
return table_to_string(self)
end
local user = User.new({
name = "Ana",
age = 30,
address = {
city = "Madrid",
zip = "28001"
}
})
print(user)
-- {
-- name: "Ana",
-- age: 30,
-- address: {
-- city: "Madrid",
-- zip: "28001",
-- },
-- }
Debug Representation
local function inspect(obj)
local mt = getmetatable(obj)
if mt and mt.__tostring then
return mt.__tostring(obj)
end
if type(obj) == "table" then
local items = {}
for k, v in pairs(obj) do
table.insert(items, string.format("%s=%s", k, inspect(v)))
end
return "{" .. table.concat(items, ", ") .. "}"
end
return tostring(obj)
end
local Complex = {}
Complex.__index = Complex
function Complex.new(real, imag)
local self = setmetatable({}, Complex)
self.real = real
self.imag = imag
return self
end
function Complex:__tostring()
if self.imag >= 0 then
return string.format("%.2f + %.2fi", self.real, self.imag)
else
return string.format("%.2f - %.2fi", self.real, -self.imag)
end
end
local c = Complex.new(3, -4)
print(c) -- 3.00 - 4.00i
5. Crear Protocolos Personalizados
Podemos definir nuestros propios protocolos mediante convenciones de nombres de métodos.
Protocolo Serializable
-- Protocolo: Un objeto es Serializable si tiene serialize() y deserialize()
local Serializable = {}
function Serializable.serialize(obj)
if type(obj) == "table" and obj.serialize then
return obj:serialize()
end
error("Objeto no es Serializable")
end
function Serializable.deserialize(cls, data)
if cls.deserialize then
return cls.deserialize(data)
end
error("Clase no puede deserializar")
end
-- Implementación
local Task = {}
Task.__index = Task
function Task.new(title, completed)
local self = setmetatable({}, Task)
self.title = title
self.completed = completed or false
return self
end
-- Implementar protocolo Serializable
function Task:serialize()
return string.format("%s|%s", self.title, tostring(self.completed))
end
function Task.deserialize(data)
local title, completed = data:match("([^|]+)|(%a+)")
return Task.new(title, completed == "true")
end
-- Uso
local task = Task.new("Aprender Lua", true)
local serialized = Serializable.serialize(task)
print(serialized) -- Aprender Lua|true
local restored = Serializable.deserialize(Task, serialized)
print(restored.title, restored.completed) -- Aprender Lua true
Protocolo Validatable
-- Protocolo: Un objeto es Validatable si tiene validate()
local Validatable = {}
function Validatable.is_valid(obj)
if type(obj) == "table" and obj.validate then
local ok, errors = obj:validate()
return ok, errors
end
return false, {"No es validatable"}
end
-- Implementación
local Email = {}
Email.__index = Email
function Email.new(address)
local self = setmetatable({}, Email)
self.address = address
return self
end
function Email:validate()
local errors = {}
if not self.address or self.address == "" then
table.insert(errors, "Email no puede estar vacío")
end
if not self.address:match("^[%w.]+@[%w.]+%.%w+$") then
table.insert(errors, "Formato de email inválido")
end
return #errors == 0, errors
end
-- Uso
local email1 = Email.new("[email protected]")
local ok1, errors1 = Validatable.is_valid(email1)
print(ok1) -- true
local email2 = Email.new("invalid-email")
local ok2, errors2 = Validatable.is_valid(email2)
print(ok2) -- false
for _, err in ipairs(errors2) do
print("- " .. err) -- - Formato de email inválido
end
Protocolo Comparable
-- Protocolo: Un objeto es Comparable si tiene compare()
local Comparable = {}
function Comparable.sort(array)
table.sort(array, function(a, b)
if a.compare and b.compare then
return a:compare(b) < 0
end
return a < b
end)
return array
end
-- Implementación
local Version = {}
Version.__index = Version
function Version.new(major, minor, patch)
local self = setmetatable({}, Version)
self.major = major
self.minor = minor
self.patch = patch
return self
end
function Version:compare(other)
if self.major ~= other.major then
return self.major - other.major
end
if self.minor ~= other.minor then
return self.minor - other.minor
end
return self.patch - other.patch
end
function Version:__tostring()
return string.format("%d.%d.%d", self.major, self.minor, self.patch)
end
-- Uso
local versions = {
Version.new(2, 1, 0),
Version.new(1, 9, 3),
Version.new(2, 0, 5),
Version.new(1, 9, 10)
}
Comparable.sort(versions)
for _, v in ipairs(versions) do
print(v) -- 1.9.3, 1.9.10, 2.0.5, 2.1.0
end
6. Duck Typing en Lua
Duck typing permite usar objetos según su comportamiento, no su tipo declarado.
Verificación de Capacidades
local function can_fly(obj)
return type(obj.fly) == "function"
end
local function can_swim(obj)
return type(obj.swim) == "function"
end
-- Diferentes implementaciones
local Bird = {}
function Bird.new()
return setmetatable({}, {__index = Bird})
end
function Bird:fly()
return "Volando alto"
end
local Duck = {}
function Duck.new()
return setmetatable({}, {__index = Duck})
end
function Duck:fly()
return "Volando bajo"
end
function Duck:swim()
return "Nadando"
end
local Fish = {}
function Fish.new()
return setmetatable({}, {__index = Fish})
end
function Fish:swim()
return "Nadando rápido"
end
-- Uso
local animals = {Bird.new(), Duck.new(), Fish.new()}
for _, animal in ipairs(animals) do
if can_fly(animal) then
print(animal:fly())
end
if can_swim(animal) then
print(animal:swim())
end
end
Polimorfismo sin Herencia
-- No necesitamos una clase base común
local function process_payment(payment_method, amount)
if type(payment_method.charge) == "function" then
return payment_method:charge(amount)
end
error("Método de pago inválido")
end
-- Implementaciones diferentes
local CreditCard = {}
CreditCard.__index = CreditCard
function CreditCard.new(number)
local self = setmetatable({}, CreditCard)
self.number = number
return self
end
function CreditCard:charge(amount)
return string.format("Cargado $%.2f a tarjeta %s", amount, self.number)
end
local PayPal = {}
PayPal.__index = PayPal
function PayPal.new(email)
local self = setmetatable({}, PayPal)
self.email = email
return self
end
function PayPal:charge(amount)
return string.format("Enviado $%.2f a PayPal: %s", amount, self.email)
end
-- Uso (polimórfico)
local cc = CreditCard.new("****-1234")
local pp = PayPal.new("[email protected]")
print(process_payment(cc, 99.99))
print(process_payment(pp, 49.99))
Adapter Pattern
-- Adaptar interfaces incompatibles
local OldLogger = {}
function OldLogger.new()
return setmetatable({}, {__index = OldLogger})
end
function OldLogger:write(message)
print("[OLD] " .. message)
end
local NewLogger = {}
function NewLogger.new()
return setmetatable({}, {__index = NewLogger})
end
function NewLogger:log(level, message)
print(string.format("[%s] %s", level, message))
end
-- Adapter para unificar interfaces
local function make_logger(obj)
if type(obj.log) == "function" then
-- Ya cumple el protocolo nuevo
return obj
elseif type(obj.write) == "function" then
-- Adaptar del protocolo viejo
return {
log = function(self, level, message)
obj:write(string.format("[%s] %s", level, message))
end
}
end
error("No es un logger válido")
end
local old = OldLogger.new()
local new = NewLogger.new()
local logger1 = make_logger(old)
local logger2 = make_logger(new)
logger1:log("INFO", "Mensaje 1") -- [OLD] [INFO] Mensaje 1
logger2:log("ERROR", "Mensaje 2") -- [ERROR] Mensaje 2
7. Verificación de Protocolos en Runtime
Podemos crear sistemas de validación para verificar si un objeto cumple un protocolo.
Sistema de Verificación
local Protocol = {}
function Protocol.new(name, methods)
local self = {
name = name,
methods = methods
}
function self:check(obj)
local missing = {}
for _, method in ipairs(self.methods) do
if type(obj[method]) ~= "function" then
table.insert(missing, method)
end
end
if #missing > 0 then
return false, string.format(
"Objeto no cumple protocolo '%s': faltan métodos %s",
self.name,
table.concat(missing, ", ")
)
end
return true
end
function self:assert(obj)
local ok, err = self:check(obj)
if not ok then
error(err)
end
end
return self
end
-- Definir protocolos
local Drawable = Protocol.new("Drawable", {"draw", "clear"})
local Movable = Protocol.new("Movable", {"move", "get_position"})
-- Implementación
local Circle = {}
Circle.__index = Circle
function Circle.new(x, y, r)
local self = setmetatable({}, Circle)
self.x = x
self.y = y
self.radius = r
return self
end
function Circle:draw()
print(string.format("Dibujando círculo en (%d,%d) r=%d", self.x, self.y, self.radius))
end
function Circle:clear()
print("Limpiando círculo")
end
function Circle:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
end
function Circle:get_position()
return self.x, self.y
end
-- Verificación
local circle = Circle.new(10, 20, 5)
local ok1 = Drawable:check(circle)
print(ok1) -- true
local ok2 = Movable:check(circle)
print(ok2) -- true
-- Intentar con objeto incompleto
local incomplete = {}
local ok3, err = Drawable:check(incomplete)
print(ok3, err) -- false Objeto no cumple protocolo 'Drawable': faltan métodos draw, clear
Type Checking Avanzado
local TypeChecker = {}
function TypeChecker.implements(obj, protocol)
for method_name, expected_type in pairs(protocol) do
local actual_type = type(obj[method_name])
if expected_type == "function" and actual_type ~= "function" then
return false, string.format("Método '%s' debe ser función", method_name)
end
if expected_type == "number" and actual_type ~= "number" then
return false, string.format("Campo '%s' debe ser número", method_name)
end
-- Más tipos según necesidad
end
return true
end
-- Protocolo con tipos
local PointProtocol = {
x = "number",
y = "number",
distance = "function"
}
local Point = {}
Point.__index = Point
function Point.new(x, y)
local self = setmetatable({}, Point)
self.x = x
self.y = y
return self
end
function Point:distance(other)
local dx = self.x - other.x
local dy = self.y - other.y
return math.sqrt(dx*dx + dy*dy)
end
-- Verificar
local p = Point.new(3, 4)
local ok, err = TypeChecker.implements(p, PointProtocol)
print(ok) -- true
Protocol Registry
local ProtocolRegistry = {}
ProtocolRegistry.protocols = {}
function ProtocolRegistry.register(name, spec)
ProtocolRegistry.protocols[name] = spec
end
function ProtocolRegistry.check(obj, protocol_name)
local spec = ProtocolRegistry.protocols[protocol_name]
if not spec then
error("Protocolo desconocido: " .. protocol_name)
end
for _, method in ipairs(spec.methods or {}) do
if type(obj[method]) ~= "function" then
return false
end
end
for _, field in ipairs(spec.fields or {}) do
if obj[field] == nil then
return false
end
end
return true
end
-- Registro
ProtocolRegistry.register("Repository", {
methods = {"save", "find", "delete"},
fields = {}
})
ProtocolRegistry.register("EventEmitter", {
methods = {"on", "emit", "off"},
fields = {}
})
-- Uso
local UserRepository = {}
UserRepository.__index = UserRepository
function UserRepository.new()
return setmetatable({}, UserRepository)
end
function UserRepository:save(user)
print("Guardando usuario:", user.name)
end
function UserRepository:find(id)
return {id = id, name = "Test"}
end
function UserRepository:delete(id)
print("Eliminando usuario:", id)
end
local repo = UserRepository.new()
print(ProtocolRegistry.check(repo, "Repository")) -- true
print(ProtocolRegistry.check(repo, "EventEmitter")) -- false
8. DEEP DIVE: Protocolos vs Interfaces
Diferencias Conceptuales
Interfaces (lenguajes estáticos):
- Declaración explícita:
class X implements Interface - Verificación en tiempo de compilación
- Contratos formales obligatorios
- Jerarquías estrictas
Protocolos (lenguajes dinámicos):
- Implementación implícita: “si tiene los métodos, cumple el protocolo”
- Verificación en tiempo de ejecución (opcional)
- Contratos por convención
- Flexibilidad total
Ventajas de Protocolos
-- 1. No necesitas modificar clases existentes
-- Ejemplo: añadir comportamiento a una tabla estándar
local array = {1, 2, 3, 4, 5}
-- Añadir protocolo "Mappable" a una tabla existente
local mt = getmetatable(array) or {}
mt.map = function(self, fn)
local result = {}
for i, v in ipairs(self) do
result[i] = fn(v)
end
return result
end
setmetatable(array, mt)
local doubled = array:map(function(x) return x * 2 end)
-- {2, 4, 6, 8, 10}
-- 2. Composición flexible sin herencia múltiple
local Walkable = {
walk = function(self)
return string.format("%s está caminando", self.name)
end
}
local Swimmable = {
swim = function(self)
return string.format("%s está nadando", self.name)
end
}
local Flyable = {
fly = function(self)
return string.format("%s está volando", self.name)
end
}
-- Componer dinámicamente
local function make_creature(name, ...)
local creature = {name = name}
for _, protocol in ipairs({...}) do
for k, v in pairs(protocol) do
creature[k] = v
end
end
return creature
end
local duck = make_creature("Pato", Walkable, Swimmable, Flyable)
print(duck:walk()) -- Pato está caminando
print(duck:swim()) -- Pato está nadando
local fish = make_creature("Pez", Swimmable)
print(fish:swim()) -- Pez está nadando
-- fish:walk() -- error, no tiene este método
Desventajas de Protocolos
-- 1. Errores en runtime, no en compilación
local function process(obj)
-- Asumimos que tiene método 'validate'
return obj:validate() -- Puede fallar aquí
end
-- Solución: verificación explícita
local function safe_process(obj)
if type(obj.validate) == "function" then
return obj:validate()
end
error("Objeto no tiene método 'validate'")
end
-- 2. Documentación implícita
-- Sin interfaces formales, debemos documentar protocolos
--[[
Protocolo: Storable
Métodos requeridos:
- save(): Guarda el objeto
- load(id): Carga el objeto por ID
- delete(id): Elimina el objeto
Campos requeridos:
- table_name: string
Ejemplo:
local obj = Storable.new()
obj:save()
]]
local Storable = {}
-- ... implementación
Estrategia Híbrida
-- Combinar lo mejor de ambos mundos
local Interface = {}
function Interface.define(name, spec)
local interface = {
name = name,
methods = spec.methods or {},
fields = spec.fields or {}
}
-- Método para implementar el interface
function interface:implement(cls)
local original_new = cls.new
cls.new = function(...)
local obj = original_new(...)
-- Verificar al crear instancia
interface:verify(obj)
return obj
end
return cls
end
-- Verificación
function interface:verify(obj)
for _, method in ipairs(self.methods) do
assert(type(obj[method]) == "function",
string.format("Falta método '%s' para interface '%s'", method, self.name))
end
for _, field in ipairs(self.fields) do
assert(obj[field] ~= nil,
string.format("Falta campo '%s' para interface '%s'", field, self.name))
end
end
return interface
end
-- Definir interface
local ILogger = Interface.define("Logger", {
methods = {"log", "error", "warn"}
})
-- Implementar
local ConsoleLogger = {}
ConsoleLogger.__index = ConsoleLogger
function ConsoleLogger.new()
return setmetatable({}, ConsoleLogger)
end
function ConsoleLogger:log(msg)
print("[LOG] " .. msg)
end
function ConsoleLogger:error(msg)
print("[ERROR] " .. msg)
end
function ConsoleLogger:warn(msg)
print("[WARN] " .. msg)
end
-- Aplicar interface (verifica en creación)
ILogger:implement(ConsoleLogger)
local logger = ConsoleLogger.new() -- OK
logger:log("Sistema iniciado")
9. SOAPBOX: Lua vs Go Interfaces
Go tiene uno de los sistemas de interfaces más elegantes en lenguajes estáticos: interfaces implícitas. Curiosamente, es muy similar al duck typing de Lua, pero con verificación en tiempo de compilación.
Go Interfaces (ejemplo conceptual)
// Go code (para comparación)
type Writer interface {
Write([]byte) (int, error)
}
// Cualquier tipo que tenga Write() implementa Writer
// No necesita declarar "implements Writer"
type FileWriter struct {}
func (f FileWriter) Write(data []byte) (int, error) {
// implementación
}
// FileWriter automáticamente implementa Writer
Equivalente en Lua
-- Protocolo Writer
local Writer = {}
function Writer.is_writer(obj)
return type(obj.write) == "function"
end
function Writer.write_all(writer, data)
if not Writer.is_writer(writer) then
error("No es un Writer válido")
end
return writer:write(data)
end
-- Implementación
local FileWriter = {}
FileWriter.__index = FileWriter
function FileWriter.new(path)
local self = setmetatable({}, FileWriter)
self.path = path
return self
end
function FileWriter:write(data)
-- Escribir a archivo
print("Escribiendo a " .. self.path .. ": " .. data)
return #data
end
local StringWriter = {}
StringWriter.__index = StringWriter
function StringWriter.new()
local self = setmetatable({}, StringWriter)
self.buffer = ""
return self
end
function StringWriter:write(data)
self.buffer = self.buffer .. data
return #data
end
-- Ambos "implementan" Writer sin declararlo
local fw = FileWriter.new("/tmp/test.txt")
local sw = StringWriter.new()
Writer.write_all(fw, "Hola") -- OK
Writer.write_all(sw, "Mundo") -- OK
Opinión Personal
Ventajas de Lua sobre Go:
- Máxima flexibilidad: Puedes modificar protocolos en runtime
- Meta-programación: Los metamétodos permiten comportamientos imposibles en Go
- No necesitas pensar en tipos: Código más rápido de escribir
Ventajas de Go sobre Lua:
- Seguridad: Errores en compilación, no en producción
- Performance: Optimizaciones del compilador
- Tooling: IDE puede autocompletar métodos de interfaces
El mejor de dos mundos: TypeScript con Lua
- Usar LuaLS (Language Server) con anotaciones de tipo
- Documentar protocolos con @class y @interface
- Verificación estática opcional con herramientas externas
---@class Writer
---@field write fun(self: Writer, data: string): number
---@type Writer
local file_writer = FileWriter.new("/tmp/test")
-- LuaLS puede verificar que file_writer tenga método 'write'
file_writer:write("test")
10. Caso Práctico: ORM con Active Record Pattern
Implementaremos un mini-ORM que usa protocolos para modelar entidades de base de datos.
Sistema Base
-- Protocolo Model: save, find, delete, update
local Model = {}
Model.__index = Model
function Model.extend(table_name, schema)
local cls = {
table_name = table_name,
schema = schema
}
setmetatable(cls, {__index = Model})
cls.__index = cls
return cls
end
function Model:new(attrs)
local instance = setmetatable({}, self)
instance.attrs = attrs or {}
instance.is_new_record = true
return instance
end
-- Protocolo Validatable
function Model:validate()
local errors = {}
for field, rules in pairs(self.schema) do
local value = self.attrs[field]
if rules.required and not value then
table.insert(errors, field .. " es requerido")
end
if rules.type and type(value) ~= rules.type then
table.insert(errors, field .. " debe ser " .. rules.type)
end
if rules.min and value < rules.min then
table.insert(errors, field .. " debe ser >= " .. rules.min)
end
end
return #errors == 0, errors
end
-- Protocolo Persistable
function Model:save()
local ok, errors = self:validate()
if not ok then
return false, errors
end
if self.is_new_record then
-- INSERT
local fields = {}
local values = {}
for k, v in pairs(self.attrs) do
table.insert(fields, k)
table.insert(values, string.format("'%s'", tostring(v)))
end
local sql = string.format(
"INSERT INTO %s (%s) VALUES (%s)",
self.table_name,
table.concat(fields, ", "),
table.concat(values, ", ")
)
print("[SQL] " .. sql)
self.is_new_record = false
self.attrs.id = math.random(1000, 9999) -- Simular ID
else
-- UPDATE
local sets = {}
for k, v in pairs(self.attrs) do
if k ~= "id" then
table.insert(sets, string.format("%s = '%s'", k, tostring(v)))
end
end
local sql = string.format(
"UPDATE %s SET %s WHERE id = %s",
self.table_name,
table.concat(sets, ", "),
self.attrs.id
)
print("[SQL] " .. sql)
end
return true
end
function Model:delete()
if self.is_new_record then
return false, "No se puede eliminar registro no guardado"
end
local sql = string.format(
"DELETE FROM %s WHERE id = %s",
self.table_name,
self.attrs.id
)
print("[SQL] " .. sql)
return true
end
-- Protocolo Queryable
function Model.where(cls, conditions)
local wheres = {}
for k, v in pairs(conditions) do
table.insert(wheres, string.format("%s = '%s'", k, tostring(v)))
end
local sql = string.format(
"SELECT * FROM %s WHERE %s",
cls.table_name,
table.concat(wheres, " AND ")
)
print("[SQL] " .. sql)
-- Simular resultado
return {
cls:new({id = 1, name = "Resultado simulado"})
}
end
function Model.find(cls, id)
local sql = string.format(
"SELECT * FROM %s WHERE id = %s",
cls.table_name,
id
)
print("[SQL] " .. sql)
-- Simular resultado
return cls:new({id = id, name = "Usuario " .. id})
end
-- Protocolo Stringable
function Model:__tostring()
local parts = {}
for k, v in pairs(self.attrs) do
table.insert(parts, string.format("%s: %s", k, tostring(v)))
end
return string.format("<%s %s>", self.table_name, table.concat(parts, ", "))
end
Definir Modelos
-- Modelo User
local User = Model.extend("users", {
name = {type = "string", required = true},
email = {type = "string", required = true},
age = {type = "number", min = 18}
})
function User:greet()
return "Hola, soy " .. self.attrs.name
end
-- Modelo Post
local Post = Model.extend("posts", {
title = {type = "string", required = true},
body = {type = "string", required = true},
user_id = {type = "number", required = true}
})
function Post:summary()
return self.attrs.title .. " (" .. #self.attrs.body .. " chars)"
end
Uso del ORM
-- Crear usuario
local user = User:new({
name = "Ana García",
email = "[email protected]",
age = 25
})
print(user) -- <users name: Ana García, email: [email protected], age: 25>
-- Guardar (genera INSERT)
local ok, err = user:save()
-- [SQL] INSERT INTO users (name, email, age) VALUES ('Ana García', '[email protected]', '25')
-- Actualizar
user.attrs.age = 26
user:save()
-- [SQL] UPDATE users SET name = 'Ana García', email = '[email protected]', age = '26' WHERE id = 7243
-- Buscar
local found_user = User.find(User, 123)
-- [SQL] SELECT * FROM users WHERE id = 123
print(found_user:greet()) -- Hola, soy Usuario 123
-- Query con condiciones
local results = User.where(User, {age = 25, name = "Ana"})
-- [SQL] SELECT * FROM users WHERE age = '25' AND name = 'Ana'
-- Validación
local invalid_user = User:new({name = "Pedro"}) -- Falta email
local ok, errors = invalid_user:save()
print(ok) -- false
for _, err in ipairs(errors) do
print("- " .. err)
end
-- - email es requerido
-- - age es requerido
-- Eliminar
user:delete()
-- [SQL] DELETE FROM users WHERE id = 7243
Relaciones (Bonus)
-- Añadir soporte para relaciones
function Model:has_many(relation_name, foreign_class, foreign_key)
self[relation_name] = function(instance)
local conditions = {}
conditions[foreign_key] = instance.attrs.id
return foreign_class.where(foreign_class, conditions)
end
end
function Model:belongs_to(relation_name, foreign_class, foreign_key)
self[relation_name] = function(instance)
local foreign_id = instance.attrs[foreign_key]
return foreign_class.find(foreign_class, foreign_id)
end
end
-- Configurar relaciones
User:has_many("posts", Post, "user_id")
Post:belongs_to("user", User, "user_id")
-- Uso
local user = User:new({id = 1, name = "Ana"})
local posts = user:posts() -- Busca todos los posts de este usuario
-- [SQL] SELECT * FROM posts WHERE user_id = '1'
local post = Post:new({id = 10, title = "Mi post", user_id = 1})
local author = post:user() -- Busca el usuario autor
-- [SQL] SELECT * FROM users WHERE id = 1
11. Ejercicios
Ejercicio 1: Protocolo EventEmitter
Implementa un protocolo EventEmitter con los métodos:
on(event, handler): Registrar listeneremit(event, data): Emitir eventooff(event, handler): Eliminar listeneronce(event, handler): Listener que se ejecuta una sola vez
local Button = {}
Button.__index = Button
function Button.new(label)
local self = setmetatable({}, Button)
self.label = label
-- Tu código aquí: inicializar sistema de eventos
return self
end
-- Implementar protocolo EventEmitter
function Button:on(event, handler)
-- Tu código aquí
end
function Button:emit(event, data)
-- Tu código aquí
end
function Button:off(event, handler)
-- Tu código aquí
end
function Button:once(event, handler)
-- Tu código aquí
end
-- Uso esperado:
local btn = Button.new("Guardar")
btn:on("click", function(data)
print("Click!", data.x, data.y)
end)
btn:once("click", function()
print("Primera vez")
end)
btn:emit("click", {x = 10, y = 20})
-- Output: Primera vez
-- Click! 10 20
btn:emit("click", {x = 5, y = 8})
-- Output: Click! 5 8 (once no se repite)
Ejercicio 2: Protocolo Pipeline
Crea un sistema de pipeline que permita encadenar transformaciones sobre datos:
local Pipeline = {}
function Pipeline.new()
-- Tu código aquí
end
function Pipeline:add(name, transform_fn)
-- Añadir paso al pipeline
-- Tu código aquí
end
function Pipeline:remove(name)
-- Remover paso
-- Tu código aquí
end
function Pipeline:execute(data)
-- Ejecutar todos los pasos en orden
-- Tu código aquí
end
-- Uso esperado:
local pipe = Pipeline.new()
pipe:add("uppercase", function(text) return text:upper() end)
pipe:add("trim", function(text) return text:match("^%s*(.-)%s*$") end)
pipe:add("exclaim", function(text) return text .. "!" end)
print(pipe:execute(" hola mundo "))
-- Output: HOLA MUNDO!
pipe:remove("uppercase")
print(pipe:execute(" hola mundo "))
-- Output: hola mundo!
Bonus: Añadir soporte para pipelines condicionales:
pipe:add_if("validate",
function(data) return #data > 0 end, -- condición
function(data) return data .. " [OK]" end -- transformación
)
Ejercicio 3: Protocolo Cacheable
Implementa un sistema de caché genérico con TTL (time-to-live):
local Cacheable = {}
function Cacheable.new(ttl_seconds)
-- Tu código aquí: inicializar caché con TTL
end
function Cacheable:get(key)
-- Obtener valor si no ha expirado
-- Tu código aquí
end
function Cacheable:set(key, value)
-- Guardar con timestamp
-- Tu código aquí
end
function Cacheable:invalidate(key)
-- Eliminar entrada
-- Tu código aquí
end
function Cacheable:clear()
-- Limpiar todo el caché
-- Tu código aquí
end
function Cacheable:wrap(fn)
-- Retornar función que usa caché automáticamente
-- Tu código aquí
end
-- Uso esperado:
local cache = Cacheable.new(5) -- 5 segundos TTL
cache:set("user:1", {name = "Ana", age = 30})
print(cache:get("user:1").name) -- Ana
-- Después de 5 segundos
os.execute("sleep 6")
print(cache:get("user:1")) -- nil (expirado)
-- Con wrap
local function expensive_operation(n)
os.execute("sleep 2")
return n * 2
end
local cached_fn = cache:wrap(expensive_operation)
print(cached_fn(10)) -- Tarda 2s, retorna 20
print(cached_fn(10)) -- Instantáneo, retorna 20 (desde caché)
Bonus: Implementar estrategias de eviction (LRU, LFU).
Conclusión
Los protocolos en Lua permiten diseñar sistemas flexibles basados en duck typing: “si se comporta como X, entonces es X”. A diferencia de interfaces formales, los protocolos se verifican en runtime y permiten composición dinámica.
Protocolos Built-in:
__pairs,__ipairs: Iterable__call: Callable__tostring: Stringable__eq,__lt: Comparable
Mejores Prácticas:
- Documentar protocolos esperados
- Verificar capacidades antes de usar (
type(obj.method) == "function") - Crear sistemas de validación para protocolos críticos
- Usar convenciones consistentes de nombres
Con protocolos personalizados puedes crear abstracciones poderosas como ORMs, sistemas de eventos, o pipelines de datos, todo sin necesidad de jerarquías de clases complejas. La flexibilidad del duck typing, combinada con verificaciones en runtime, ofrece un balance ideal entre simplicidad y seguridad.