← Volver al listado de tecnologías

Capítulo 13: Protocolos y Duck Typing

Por: Artiko
luaprotocolsduck-typinginterfaces

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

Protocolos (lenguajes dinámicos):

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:

  1. Máxima flexibilidad: Puedes modificar protocolos en runtime
  2. Meta-programación: Los metamétodos permiten comportamientos imposibles en Go
  3. No necesitas pensar en tipos: Código más rápido de escribir

Ventajas de Go sobre Lua:

  1. Seguridad: Errores en compilación, no en producción
  2. Performance: Optimizaciones del compilador
  3. Tooling: IDE puede autocompletar métodos de interfaces

El mejor de dos mundos: TypeScript con Lua

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

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:

Mejores Prácticas:

  1. Documentar protocolos esperados
  2. Verificar capacidades antes de usar (type(obj.method) == "function")
  3. Crear sistemas de validación para protocolos críticos
  4. 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.