Capítulo 10: Metatablas - El Protocolo de Objetos
Capítulo 10: Metatablas - El Protocolo de Objetos
“Los objetos sin clases son como funciones sin nombres: más simples, más honestos.” — Roberto Ierusalimschy
Las metatablas son el mecanismo fundamental de Lua para extender el comportamiento de las tablas. Son el corazón de la orientación a objetos, sobrecarga de operadores, propiedades computadas, y mucho más.
A diferencia de lenguajes con clases (Java, Python), Lua usa prototype-based OOP, similar a JavaScript. Las metatablas son el protocolo que hace esto posible.
¿Qué es una Metatabla?
Una metatabla es una tabla especial que define cómo una tabla se comporta en ciertas operaciones. Puedes establecer la metatabla de cualquier tabla usando setmetatable:
local t = {x = 10}
local mt = {
__index = function(table, key)
return "key not found: " .. key
end
}
setmetatable(t, mt)
-- >>> print(t.x)
-- 10
-- >>> print(t.y)
-- key not found: y
La función getmetatable obtiene la metatabla de una tabla:
-- >>> print(getmetatable(t) == mt)
-- true
Metamétodo __index: Lectura de Campos
__index se invoca cuando accedes a un campo que no existe en la tabla.
__index como Función
local defaults = {
name = "Anonymous",
age = 0,
city = "Unknown"
}
local person = {}
setmetatable(person, {
__index = function(t, key)
return defaults[key]
end
})
-- >>> print(person.name)
-- Anonymous
-- >>> person.name = "Alice"
-- >>> print(person.name)
-- Alice
__index como Tabla (Más Común)
local defaults = {
name = "Anonymous",
age = 0,
city = "Unknown"
}
local person = {}
setmetatable(person, {__index = defaults})
-- >>> print(person.name)
-- Anonymous
-- >>> print(person.age)
-- 0
Este patrón es la base de la herencia por prototipos en Lua.
Metamétodo __newindex: Escritura en Campos
__newindex se invoca cuando intentas asignar a un campo que no existe.
local proxy = {}
local data = {}
setmetatable(proxy, {
__newindex = function(t, key, value)
print(string.format("Setting %s = %s", key, tostring(value)))
data[key] = value
end,
__index = function(t, key)
print(string.format("Getting %s", key))
return data[key]
end
})
-- >>> proxy.name = "Alice"
-- Setting name = Alice
-- >>> print(proxy.name)
-- Getting name
-- Alice
Read-Only Tables
local function readonly(t)
return setmetatable({}, {
__index = t,
__newindex = function(table, key, value)
error("Attempt to modify read-only table")
end
})
end
-- >>> local config = readonly({host = "localhost", port = 8080})
-- >>> print(config.host)
-- localhost
-- >>> config.host = "example.com"
-- Error: Attempt to modify read-only table
Metamétodo __call: Tablas Llamables
__call permite llamar a una tabla como si fuera una función.
local Calculator = {}
function Calculator.new(initial)
local self = {value = initial or 0}
setmetatable(self, {
__call = function(t, operation, operand)
if operation == "add" then
t.value = t.value + operand
elseif operation == "sub" then
t.value = t.value - operand
elseif operation == "mul" then
t.value = t.value * operand
elseif operation == "div" then
t.value = t.value / operand
end
return t.value
end
})
return self
end
-- >>> local calc = Calculator.new(10)
-- >>> print(calc("add", 5))
-- 15
-- >>> print(calc("mul", 2))
-- 30
Constructor Pattern
local Person = {}
setmetatable(Person, {
__call = function(cls, name, age)
local self = {name = name, age = age}
setmetatable(self, {__index = cls})
return self
end
})
function Person:greet()
return "Hello, I'm " .. self.name
end
-- >>> local alice = Person("Alice", 30)
-- >>> print(alice:greet())
-- Hello, I'm Alice
Metamétodo __tostring: Representación en String
__tostring define cómo se convierte la tabla a string con tostring().
local Point = {}
Point.__index = Point
function Point.new(x, y)
local self = {x = x, y = y}
setmetatable(self, Point)
return self
end
function Point:__tostring()
return string.format("Point(%d, %d)", self.x, self.y)
end
-- >>> local p = Point.new(10, 20)
-- >>> print(p)
-- Point(10, 20)
-- >>> print(tostring(p))
-- Point(10, 20)
Metamétodo __len: Operador #
__len personaliza el operador de longitud #.
local Queue = {}
Queue.__index = Queue
function Queue.new()
return setmetatable({items = {}}, Queue)
end
function Queue:push(value)
table.insert(self.items, value)
end
function Queue:pop()
return table.remove(self.items, 1)
end
function Queue:__len()
return #self.items
end
-- >>> local q = Queue.new()
-- >>> q:push(10)
-- >>> q:push(20)
-- >>> q:push(30)
-- >>> print(#q)
-- 3
Metamétodos Aritméticos
Puedes sobrecargar operadores matemáticos:
__add:+__sub:-__mul:*__div:/__mod:%__pow:^__unm:-(negación unaria)__idiv://(división entera, Lua 5.3+)
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
function Vector:__add(other)
return Vector.new(self.x + other.x, self.y + other.y)
end
function Vector:__sub(other)
return Vector.new(self.x - other.x, self.y - other.y)
end
function Vector:__mul(scalar)
if type(scalar) == "number" then
return Vector.new(self.x * scalar, self.y * scalar)
end
end
function Vector:__unm()
return Vector.new(-self.x, -self.y)
end
function Vector:__tostring()
return string.format("Vector(%.2f, %.2f)", self.x, self.y)
end
-- >>> local v1 = Vector.new(3, 4)
-- >>> local v2 = Vector.new(1, 2)
-- >>> print(v1 + v2)
-- Vector(4.00, 6.00)
-- >>> print(v1 - v2)
-- Vector(2.00, 2.00)
-- >>> print(v1 * 2)
-- Vector(6.00, 8.00)
-- >>> print(-v1)
-- Vector(-3.00, -4.00)
Metamétodos de Comparación
__eq:==__lt:<__le:<=
local Date = {}
Date.__index = Date
function Date.new(year, month, day)
return setmetatable({year = year, month = month, day = day}, Date)
end
function Date:to_days()
-- Simplificado: asume todos los meses tienen 30 días
return self.year * 365 + self.month * 30 + self.day
end
function Date:__eq(other)
return self:to_days() == other:to_days()
end
function Date:__lt(other)
return self:to_days() < other:to_days()
end
function Date:__le(other)
return self:to_days() <= other:to_days()
end
function Date:__tostring()
return string.format("%04d-%02d-%02d", self.year, self.month, self.day)
end
-- >>> local d1 = Date.new(2025, 1, 15)
-- >>> local d2 = Date.new(2025, 1, 20)
-- >>> print(d1 < d2)
-- true
-- >>> print(d1 == d2)
-- false
-- >>> print(d1 <= d2)
-- true
Metamétodo __concat: Operador ..
local StringBuilder = {}
StringBuilder.__index = StringBuilder
function StringBuilder.new(str)
return setmetatable({parts = {str or ""}}, StringBuilder)
end
function StringBuilder:__concat(other)
local result = StringBuilder.new()
for _, part in ipairs(self.parts) do
table.insert(result.parts, part)
end
if type(other) == "string" then
table.insert(result.parts, other)
elseif getmetatable(other) == StringBuilder then
for _, part in ipairs(other.parts) do
table.insert(result.parts, part)
end
end
return result
end
function StringBuilder:__tostring()
return table.concat(self.parts)
end
-- >>> local sb1 = StringBuilder.new("Hello")
-- >>> local sb2 = StringBuilder.new(" World")
-- >>> local sb3 = sb1 .. sb2 .. "!"
-- >>> print(sb3)
-- Hello World!
Metamétodos de Iteración (Lua 5.2+)
__pairs: Personalizar pairs()
local OrderedMap = {}
OrderedMap.__index = OrderedMap
function OrderedMap.new()
return setmetatable({
keys = {},
values = {}
}, OrderedMap)
end
function OrderedMap:set(key, value)
if not self.values[key] then
table.insert(self.keys, key)
end
self.values[key] = value
end
function OrderedMap:__pairs()
local i = 0
return function()
i = i + 1
local key = self.keys[i]
if key then
return key, self.values[key]
end
end
end
-- >>> local map = OrderedMap.new()
-- >>> map:set("z", 1)
-- >>> map:set("a", 2)
-- >>> map:set("m", 3)
-- >>> for k, v in pairs(map) do
-- >>> print(k, v)
-- >>> end
-- z 1
-- a 2
-- m 3
-- (mantiene orden de inserción)
Metamétodo __metatable: Proteger Metatabla
__metatable oculta la metatabla real:
local secret = {x = 10}
setmetatable(secret, {
__metatable = "This metatable is protected",
__index = function(t, k)
return "secret value"
end
})
-- >>> print(getmetatable(secret))
-- This metatable is protected
-- >>> setmetatable(secret, {})
-- Error: cannot change a protected metatable
DEEP DIVE: Cómo Lua Busca Metamétodos
Cuando ejecutas una operación como a + b, Lua:
- Verifica si
atiene metatabla con__add - Si no, verifica si
btiene metatabla con__add - Si alguno lo tiene, llama al metamétodo
- Si ninguno lo tiene, lanza error
local v1 = Vector.new(1, 2)
local n = 5
-- >>> print(v1 * n) -- Funciona: v1 tiene __mul
-- Vector(5.00, 10.00)
-- >>> print(n * v1) -- Error: number no tiene __mul para Vector
-- Error: attempt to multiply a number with a table
Solución: Manejar Ambos Casos
function Vector:__mul(other)
if type(other) == "number" then
return Vector.new(self.x * other, self.y * other)
elseif type(self) == "number" then
-- self es el número, other es el vector
return Vector.new(other.x * self, other.y * self)
end
end
-- Necesitas también:
Vector.__mul = function(a, b)
if type(a) == "number" then
return Vector.new(b.x * a, b.y * a)
elseif type(b) == "number" then
return Vector.new(a.x * b, a.y * b)
end
end
Caso Práctico: Vector2D Completo
local Vector2D = {}
Vector2D.__index = Vector2D
function Vector2D.new(x, y)
return setmetatable({x = x or 0, y = y or 0}, Vector2D)
end
-- Aritmética
function Vector2D:__add(other)
return Vector2D.new(self.x + other.x, self.y + other.y)
end
function Vector2D:__sub(other)
return Vector2D.new(self.x - other.x, self.y - other.y)
end
function Vector2D.__mul(a, b)
if type(a) == "number" then
return Vector2D.new(b.x * a, b.y * a)
elseif type(b) == "number" then
return Vector2D.new(a.x * b, a.y * b)
else
-- Producto punto
return a.x * b.x + a.y * b.y
end
end
function Vector2D.__div(a, b)
if type(b) == "number" then
return Vector2D.new(a.x / b, a.y / b)
end
error("Cannot divide vector by vector")
end
function Vector2D:__unm()
return Vector2D.new(-self.x, -self.y)
end
-- Comparación
function Vector2D:__eq(other)
return self.x == other.x and self.y == other.y
end
-- Métodos
function Vector2D:length()
return math.sqrt(self.x * self.x + self.y * self.y)
end
function Vector2D:normalize()
local len = self:length()
if len > 0 then
return self / len
end
return Vector2D.new(0, 0)
end
function Vector2D:dot(other)
return self.x * other.x + self.y * other.y
end
function Vector2D:__tostring()
return string.format("Vector2D(%.2f, %.2f)", self.x, self.y)
end
-- >>> local v1 = Vector2D.new(3, 4)
-- >>> local v2 = Vector2D.new(1, 2)
-- >>> print(v1 + v2)
-- Vector2D(4.00, 6.00)
-- >>> print(v1 * 2)
-- Vector2D(6.00, 8.00)
-- >>> print(2 * v1)
-- Vector2D(6.00, 8.00)
-- >>> print(v1:length())
-- 5.0
-- >>> print(v1:normalize())
-- Vector2D(0.60, 0.80)
Caso Práctico: Proxy Table
local function create_proxy(target, handler)
local proxy = {}
setmetatable(proxy, {
__index = function(t, key)
if handler.get then
return handler.get(target, key)
end
return target[key]
end,
__newindex = function(t, key, value)
if handler.set then
handler.set(target, key, value)
else
target[key] = value
end
end
})
return proxy
end
-- >>> local data = {name = "Alice", age = 30}
-- >>> local proxy = create_proxy(data, {
-- >>> get = function(target, key)
-- >>> print("Reading: " .. key)
-- >>> return target[key]
-- >>> end,
-- >>> set = function(target, key, value)
-- >>> print(string.format("Writing: %s = %s", key, tostring(value)))
-- >>> target[key] = value
-- >>> end
-- >>> })
-- >>> print(proxy.name)
-- Reading: name
-- Alice
-- >>> proxy.age = 31
-- Writing: age = 31
Caso Práctico: Default Values Factory
local function with_defaults(defaults)
return function(values)
values = values or {}
return setmetatable(values, {__index = defaults})
end
end
-- >>> local create_config = with_defaults({
-- >>> host = "localhost",
-- >>> port = 8080,
-- >>> timeout = 30
-- >>> })
-- >>> local config1 = create_config({host = "example.com"})
-- >>> print(config1.host, config1.port)
-- example.com 8080
-- >>> local config2 = create_config({port = 3000})
-- >>> print(config2.host, config2.port)
-- localhost 3000
Caso Práctico: Auto-Vivification
Crear tablas anidadas automáticamente:
local function autovivify(t)
return setmetatable(t or {}, {
__index = function(table, key)
table[key] = autovivify({})
return table[key]
end
})
end
-- >>> local data = autovivify()
-- >>> data.users.alice.age = 30
-- >>> data.users.alice.city = "Madrid"
-- >>> data.users.bob.age = 25
-- >>> print(data.users.alice.age)
-- 30
SOAPBOX: Metatablas vs Reflexión
En lenguajes como Python, usas
__getattr__y__setattr__. En Ruby,method_missing. En JavaScript, Proxies. Lua eligió un enfoque más simple: una tabla con funciones hook. Es menos mágico, más explícito, más rápido.
Resumen del Capítulo
Metatablas en Lua:
__index: Lectura de campos inexistentes (base de herencia)__newindex: Escritura en campos inexistentes (proxies, validación)__call: Hacer tablas llamables (constructores, functors)__tostring: Representación en string (tostring(),print())- Operadores aritméticos:
__add,__sub,__mul,__div, etc. - Operadores de comparación:
__eq,__lt,__le __len: Operador#personalizado__pairs: Iteración personalizada__metatable: Proteger metatabla
Próximo: Capítulo 11: Coroutines - Concurrencia Cooperativa
Ejercicios
- Implementar Stack con Metatablas: Crear una pila con operadores sobrecargados.
local Stack = {}
Stack.__index = Stack
function Stack.new()
-- Tu código aquí
end
-- Implementar:
-- __len: retornar tamaño
-- __tostring: representación visual
-- push() y pop() como métodos
-- >>> local s = Stack.new()
-- >>> s:push(10)
-- >>> s:push(20)
-- >>> print(#s)
-- 2
-- >>> print(s)
-- Stack[10, 20]
- Observable Table: Tabla que notifica cambios.
function observable(t, on_change)
-- Tu código aquí (usar __index y __newindex)
end
-- >>> local data = observable({}, function(key, old_val, new_val)
-- >>> print(string.format("%s changed: %s -> %s", key, old_val, new_val))
-- >>> end)
-- >>> data.name = "Alice"
-- name changed: nil -> Alice
-- >>> data.name = "Bob"
-- name changed: Alice -> Bob
- Complex Numbers: Implementar números complejos con todas las operaciones.
local Complex = {}
Complex.__index = Complex
function Complex.new(real, imag)
-- Tu código aquí
end
-- Implementar: __add, __sub, __mul, __div, __eq, __tostring
-- Bonus: módulo y conjugado
-- >>> local c1 = Complex.new(3, 4)
-- >>> local c2 = Complex.new(1, 2)
-- >>> print(c1 + c2)
-- 4+6i
-- >>> print(c1 * c2)
-- -5+10i
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 13: Metatables and Metamethods
- Lua 5.4 Reference Manual - Section 2.4: Metatables
- Prototype-based OOP