Lua no tiene clases. No tiene operadores sobrecargados en el sentido de C++ ni una jerarquía de tipos integrada. Lo que tiene son metatables: un mecanismo que permite asociar a cualquier tabla (o userdata) otra tabla con funciones especiales que controlan cómo se comporta la primera cuando se opera sobre ella. Es un sistema de metaprogramación minimalista que, manejado bien, da acceso a orientación a objetos, operadores personalizados y proxies transparentes.
Cómo funciona una metatable
Cada tabla puede tener asociada una metatable. Cuando Lua no sabe cómo ejecutar una operación sobre una tabla (sumarla, acceder a un campo que no existe, llamarla como función ), mira si la metatable tiene un metamétodo para esa operación. Si lo hay, lo llama; si no, lanza un error.
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt) -- true
Metamétodos de acceso: __index y __newindex
__index es el metamétodo más importante. Se activa cuando se accede a una clave que no existe en la tabla:
local defaults = {color = "rojo", tamanyo = 10, activo = true}
local objeto = setmetatable({tamanyo = 20}, {__index = defaults})
print(objeto.tamanyo) -- 20 (existe en objeto)
print(objeto.color) -- rojo (no existe en objeto, cae a defaults.__index)
print(objeto.activo) -- true
-- __index también puede ser una función:
local proxy = setmetatable({}, {
__index = function(t, clave)
print("Accediendo a clave inexistente: " .. tostring(clave))
return nil
end
})
local x = proxy.cualquiercosa
-- Imprime: "Accediendo a clave inexistente: cualquiercosa"
__newindex se activa cuando se asigna a una clave que no existe en la tabla. Sirve para hacer tablas de solo lectura o para interceptar asignaciones:
local soloLectura = setmetatable({x = 1, y = 2}, {
__newindex = function(t, k, v)
error("Esta tabla es de solo lectura: intento de asignar '" .. k .. "'")
end
})
print(soloLectura.x) -- 1 (lectura OK)
soloLectura.z = 3 -- ERROR: Esta tabla es de solo lectura: intento de asignar 'z'
Operadores aritméticos y de comparación
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
-- Suma de vectores
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
-- Producto por escalar
function Vector.__mul(v, escalar)
if type(escalar) == "number" then
return Vector.new(v.x * escalar, v.y * escalar)
elseif type(v) == "number" then
return Vector.new(escalar.x * v, escalar.y * v)
end
end
-- Longitud con #
function Vector.__len(v)
return math.sqrt(v.x * v.x + v.y * v.y)
end
-- Representación como string
function Vector.__tostring(v)
return string.format("(%g, %g)", v.x, v.y)
end
-- Igualdad
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)
local v3 = v1 + v2
print(tostring(v3)) -- (4, 6)
print(#v1) -- 5.0 (3-4-5)
print(v1 * 2) -- (6, 8) gracias a __tostring en print
print(v1 == Vector.new(3, 4)) -- true
Orientación a objetos: el patrón estándar
El patrón más extendido para OOP en Lua usa __index = ClaseBase para que las instancias hereden los métodos de la clase:
-- Clase base: Animal
local Animal = {}
Animal.__index = Animal
function Animal.new(nombre, sonido)
local self = setmetatable({}, Animal)
self.nombre = nombre
self.sonido = sonido
self.energia = 100
return self
end
function Animal:hablar()
print(self.nombre .. " dice: " .. self.sonido)
end
function Animal:comer(cantidad)
self.energia = self.energia + cantidad
print(self.nombre .. " come. Energía: " .. self.energia)
end
function Animal:__tostring()
return "Animal(" .. self.nombre .. ")"
end
-- Subclase: Perro
local Perro = setmetatable({}, {__index = Animal})
Perro.__index = Perro
function Perro.new(nombre)
-- Llamar al constructor del padre
local self = Animal.new(nombre, "Guau")
return setmetatable(self, Perro)
end
-- Método específico de Perro
function Perro:buscarPalo()
self.energia = self.energia - 10
print(self.nombre .. " busca el palo. Energía: " .. self.energia)
end
-- Sobreescribir método del padre
function Perro:hablar()
Animal.hablar(self) -- llamar al método del padre
print("(mueve la cola)")
end
local rex = Perro.new("Rex")
rex:hablar() -- Rex dice: Guau n (mueve la cola)
rex:comer(20) -- Rex come. Energía: 120
rex:buscarPalo() -- Rex busca el palo. Energía: 110
print(rex.nombre) -- Rex
El metamétodo __call: tablas que se llaman como funciones
-- Función memoizada usando __call
local Memoize = {}
Memoize.__index = Memoize
function Memoize.new(fn)
return setmetatable({fn = fn, cache = {}}, Memoize)
end
function Memoize:__call(...)
local key = table.concat({...}, ",")
if self.cache[key] == nil then
self.cache[key] = self.fn(...)
end
return self.cache[key]
end
local fibonacci = Memoize.new(function(n)
if n <= 1 then return n end
-- OJO: aquí fibonacci ya está en scope gracias al upvalue
return fibonacci(n-1) + fibonacci(n-2)
end)
print(fibonacci(10)) -- 55
print(fibonacci(30)) -- 832040 (rápido gracias al cache)
Las metatables son la pieza que conecta la simplicidad de las tablas de Lua con patrones sofisticados como la herencia prototipal, los proxies reactivos o los DSL embebidos. Muchos frameworks de Lua, incluidos los de LÖVE y Defold, usan este patrón para sus APIs de objetos de juego. Una vez que se entiende cómo funciona __index, el resto del ecosistema Lua resulta mucho más legible.
Imagen: Pexels / César Gaviria
