Ir al contenido principal

Metatablas y metamétodos en Lua

Las metatablas representan uno de los mecanismos más potentes y característicos de Lua, permitiendo modificar el comportamiento por defecto de las tablas y personalizar prácticamente cualquier operación que se realice sobre ellas. A través de los metamétodos, podemos dotar a nuestras estructuras de datos de capacidades avanzadas que van desde la sobrecarga de operadores hasta la implementación de sistemas completos de programación orientada a objetos.

En este artículo exploraremos qué son las metatablas, cómo funcionan los metamétodos y cómo aprovechar estas características para crear código más expresivo y potente. Descubrirás que las metatablas son la clave para desbloquear todo el potencial de Lua como lenguaje de programación.

¿Qué son las metatablas?

Una metatabla es una tabla especial que se asocia a otra tabla para modificar su comportamiento. Cuando intentamos realizar una operación sobre una tabla que no está definida directamente en ella, Lua busca en su metatabla para ver si existe un metamétodo que especifique cómo debe comportarse esa operación.

Podemos pensar en las metatablas como una especie de "manual de instrucciones" que le dice a Lua cómo debe comportarse una tabla cuando se realizan operaciones especiales sobre ella. Este mecanismo es lo que permite a Lua ser tan flexible y expresivo.

Cada tabla en Lua puede tener su propia metatabla, y múltiples tablas pueden compartir la misma metatabla. Esto permite definir comportamientos comunes para un conjunto de tablas sin tener que duplicar código.

Funciones básicas para trabajar con metatablas

Lua proporciona dos funciones fundamentales para trabajar con metatablas:

setmetatable(tabla, metatabla): Esta función asigna una metatabla a una tabla. Devuelve la tabla modificada.

getmetatable(tabla): Esta función devuelve la metatabla asociada a una tabla, o nil si la tabla no tiene metatabla.

Veamos un ejemplo básico de cómo asignar y recuperar una metatabla:

-- Creamos una tabla normal
miTabla = {valor = 10}

-- Creamos una metatabla
miMetatabla = {nombre = "Mi primera metatabla"}

-- Asignamos la metatabla a la tabla
setmetatable(miTabla, miMetatabla)

-- Recuperamos la metatabla
mt = getmetatable(miTabla)
print(mt.nombre)  --> Mi primera metatabla

En este ejemplo hemos creado una tabla y le hemos asignado una metatabla. La metatabla en sí es simplemente otra tabla que contiene información o funciones especiales.

Metamétodos básicos

Los metamétodos son funciones especiales que se definen en las metatablas y que Lua invoca automáticamente cuando se realizan ciertas operaciones. Los nombres de los metamétodos siempre comienzan con dos guiones bajos __.

El metamétodo __index

El metamétodo __index es probablemente el más utilizado. Se invoca cuando intentamos acceder a una clave que no existe en la tabla original. Puede ser una función o una tabla.

Usando __index como tabla:

-- Tabla con valores por defecto
valoresPorDefecto = {
  color = "rojo",
  tamano = "mediano"
}

-- Metatabla que usa __index
metatabla = {
  __index = valoresPorDefecto
}

-- Creamos un objeto
coche = {marca = "Ford"}
setmetatable(coche, metatabla)

print(coche.marca)   --> Ford (existe en la tabla)
print(coche.color)   --> rojo (se busca en __index)
print(coche.tamano)  --> mediano (se busca en __index)

En este ejemplo, cuando intentamos acceder a coche.color, Lua no encuentra esa clave en la tabla coche, así que busca en la metatabla y ejecuta __index, que en este caso apunta a la tabla valoresPorDefecto.

Usando __index como función:

metatabla = {
  __index = function(tabla, clave)
    return "La clave '" .. clave .. "' no existe"
  end
}

objeto = {}
setmetatable(objeto, metatabla)

print(objeto.cualquierCosa)  --> La clave 'cualquierCosa' no existe

Cuando __index es una función, recibe dos argumentos: la tabla original y la clave que se está buscando. Esto nos permite implementar lógica personalizada para valores no definidos.

El metamétodo __newindex

El metamétodo __newindex se invoca cuando intentamos asignar un valor a una clave que no existe en la tabla original. Al igual que __index, puede ser una función o una tabla.

-- Tabla para almacenar valores
almacen = {}

metatabla = {
  __newindex = function(tabla, clave, valor)
    print("Asignando " .. valor .. " a la clave " .. clave)
    -- Guardamos en otra tabla
    almacen[clave] = valor
  end,
  
  __index = almacen
}

objeto = {}
setmetatable(objeto, metatabla)

objeto.nombre = "Juan"  --> Asignando Juan a la clave nombre
print(objeto.nombre)    --> Juan

Este ejemplo muestra cómo podemos interceptar las asignaciones y redirigirlas a otra tabla. Esto es útil para implementar sistemas de validación o logging.

Protegiendo tablas con __newindex

Podemos usar __newindex para crear tablas de solo lectura:

function crearTablaSoloLectura(tabla)
  local proxy = {}
  local metatabla = {
    __index = tabla,
    __newindex = function(t, clave, valor)
      error("Intento de modificar una tabla de solo lectura")
    end
  }
  setmetatable(proxy, metatabla)
  return proxy
end

configuracion = {host = "localhost", puerto = 8080}
config = crearTablaSoloLectura(configuracion)

print(config.host)      --> localhost
-- config.host = "192.168.1.1"  --> Error: Intento de modificar una tabla de solo lectura

Sobrecarga de operadores aritméticos

Lua permite sobrecargar los operadores aritméticos mediante metamétodos específicos. Esto nos permite definir cómo deben comportarse las operaciones matemáticas con nuestras tablas personalizadas.

Metamétodos aritméticos disponibles

Los metamétodos para operadores aritméticos son:

  • __add: Suma (+)
  • __sub: Resta (-)
  • __mul: Multiplicación (*)
  • __div: División (/)
  • __mod: Módulo (%)
  • __pow: Potencia (^)
  • __unm: Negación unaria (-)

Veamos un ejemplo completo implementando una clase Vector2D con operaciones matemáticas:

-- Constructor para vectores 2D
function nuevoVector(x, y)
  local vector = {x = x or 0, y = y or 0}
  
  local metatabla = {
    -- Suma de vectores
    __add = function(v1, v2)
      return nuevoVector(v1.x + v2.x, v1.y + v2.y)
    end,
    
    -- Resta de vectores
    __sub = function(v1, v2)
      return nuevoVector(v1.x - v2.x, v1.y - v2.y)
    end,
    
    -- Multiplicación por escalar
    __mul = function(v, escalar)
      if type(v) == "number" then
        v, escalar = escalar, v
      end
      return nuevoVector(v.x * escalar, v.y * escalar)
    end,
    
    -- Negación
    __unm = function(v)
      return nuevoVector(-v.x, -v.y)
    end,
    
    -- Representación como cadena
    __tostring = function(v)
      return "(" .. v.x .. ", " .. v.y .. ")"
    end
  }
  
  setmetatable(vector, metatabla)
  return vector
end

-- Probamos los vectores
v1 = nuevoVector(3, 4)
v2 = nuevoVector(1, 2)

v3 = v1 + v2
print(v3)           --> (4, 6)

v4 = v1 - v2
print(v4)           --> (2, 2)

v5 = v1 * 2
print(v5)           --> (6, 8)

v6 = -v1
print(v6)           --> (-3, -4)

Este ejemplo muestra cómo las metatablas nos permiten crear tipos de datos completamente nuevos con operaciones matemáticas naturales y expresivas.

Sobrecarga de operadores relacionales

Los operadores de comparación también pueden sobrecargarse mediante metamétodos:

  • __eq: Igualdad (==)
  • __lt: Menor que (<)
  • __le: Menor o igual que (<=)

Es importante notar que Lua deriva automáticamente los operadores > y >= a partir de __lt y __le, y ~= se deriva de __eq.

function nuevoRectangulo(ancho, alto)
  local rectangulo = {ancho = ancho, alto = alto}
  
  local metatabla = {
    -- Calculamos el área
    area = function(r)
      return r.ancho * r.alto
    end,
    
    -- Dos rectángulos son iguales si tienen la misma área
    __eq = function(r1, r2)
      local mt = getmetatable(r1)
      return mt.area(r1) == mt.area(r2)
    end,
    
    -- Un rectángulo es menor si tiene menor área
    __lt = function(r1, r2)
      local mt = getmetatable(r1)
      return mt.area(r1) < mt.area(r2)
    end,
    
    __le = function(r1, r2)
      local mt = getmetatable(r1)
      return mt.area(r1) <= mt.area(r2)
    end,
    
    __tostring = function(r)
      return "Rectangulo(" .. r.ancho .. "x" .. r.alto .. ")"
    end
  }
  
  setmetatable(rectangulo, metatabla)
  return rectangulo
end

r1 = nuevoRectangulo(4, 5)   -- área = 20
r2 = nuevoRectangulo(2, 10)  -- área = 20
r3 = nuevoRectangulo(3, 6)   -- área = 18

print(r1 == r2)  --> true (misma área)
print(r1 > r3)   --> true (20 > 18)
print(r3 <= r1)  --> true (18 <= 20)

El metamétodo __tostring

El metamétodo __tostring se invoca cuando intentamos convertir una tabla a cadena, generalmente al usar print() o tostring(). Esto es especialmente útil para depuración y logging.

function nuevaPersona(nombre, edad)
  local persona = {nombre = nombre, edad = edad}
  
  local metatabla = {
    __tostring = function(p)
      return p.nombre .. " tiene " .. p.edad .. " años"
    end
  }
  
  setmetatable(persona, metatabla)
  return persona
end

juan = nuevaPersona("Juan", 30)
maria = nuevaPersona("Maria", 25)

print(juan)   --> Juan tiene 30 años
print(maria)  --> Maria tiene 25 años

El metamétodo __call

El metamétodo __call permite que una tabla se comporte como si fuera una función. Cuando intentamos "llamar" a una tabla, Lua busca este metamétodo y lo ejecuta.

function crearContador(valorInicial)
  local contador = {valor = valorInicial or 0}
  
  local metatabla = {
    __call = function(c, incremento)
      c.valor = c.valor + (incremento or 1)
      return c.valor
    end
  }
  
  setmetatable(contador, metatabla)
  return contador
end

contador = crearContador(10)

print(contador())      --> 11 (incrementa en 1)
print(contador(5))     --> 16 (incrementa en 5)
print(contador())      --> 17 (incrementa en 1)

Este metamétodo es particularmente útil para crear objetos que actúan como funciones o para implementar patrones como el patrón Strategy.

El metamétodo __concat

El metamétodo __concat se invoca cuando usamos el operador de concatenación .. con tablas.

function nuevaCadenaPersonalizada(texto)
  local objeto = {texto = texto}
  
  local metatabla = {
    __concat = function(a, b)
      local textoA = type(a) == "table" and a.texto or a
      local textoB = type(b) == "table" and b.texto or b
      return nuevaCadenaPersonalizada(textoA .. " " .. textoB)
    end,
    
    __tostring = function(obj)
      return obj.texto
    end
  }
  
  setmetatable(objeto, metatabla)
  return objeto
end

saludo = nuevaCadenaPersonalizada("Hola")
nombre = nuevaCadenaPersonalizada("Mundo")

mensaje = saludo .. nombre
print(mensaje)  --> Hola Mundo

mensaje2 = mensaje .. "!"
print(mensaje2)  --> Hola Mundo !

El metamétodo __len

El metamétodo __len permite personalizar el comportamiento del operador de longitud # para nuestras tablas.

function nuevaLista()
  local lista = {elementos = {}, tamano = 0}
  
  local metatabla = {
    agregar = function(l, elemento)
      l.tamano = l.tamano + 1
      l.elementos[l.tamano] = elemento
    end,
    
    __len = function(l)
      return l.tamano
    end,
    
    __tostring = function(l)
      local resultado = "Lista["
      for i = 1, l.tamano do
        resultado = resultado .. tostring(l.elementos[i])
        if i < l.tamano then
          resultado = resultado .. ", "
        end
      end
      return resultado .. "]"
    end
  }
  
  setmetatable(lista, metatabla)
  return lista
end

miLista = nuevaLista()
local mt = getmetatable(miLista)

mt.agregar(miLista, "manzana")
mt.agregar(miLista, "pera")
mt.agregar(miLista, "naranja")

print(#miLista)    --> 3
print(miLista)     --> Lista[manzana, pera, naranja]

Implementación de orientación a objetos con metatablas

Las metatablas son la base para implementar programación orientada a objetos en Lua. Podemos crear clases con herencia, métodos y propiedades usando este mecanismo.

-- Definición de la clase base
Vehiculo = {}
Vehiculo.__index = Vehiculo

function Vehiculo:nuevo(marca, modelo)
  local instancia = setmetatable({}, self)
  instancia.marca = marca
  instancia.modelo = modelo
  instancia.velocidad = 0
  return instancia
end

function Vehiculo:acelerar(incremento)
  self.velocidad = self.velocidad + incremento
  print(self.marca .. " " .. self.modelo .. " acelera a " .. self.velocidad .. " km/h")
end

function Vehiculo:frenar(decremento)
  self.velocidad = math.max(0, self.velocidad - decremento)
  print(self.marca .. " " .. self.modelo .. " frena a " .. self.velocidad .. " km/h")
end

-- Definición de una clase derivada
Coche = setmetatable({}, {__index = Vehiculo})
Coche.__index = Coche

function Coche:nuevo(marca, modelo, puertas)
  local instancia = Vehiculo.nuevo(self, marca, modelo)
  instancia.puertas = puertas
  return instancia
end

function Coche:tocarClaxon()
  print("¡Beep beep!")
end

-- Creamos instancias
miCoche = Coche:nuevo("Toyota", "Corolla", 4)
miCoche:acelerar(50)    --> Toyota Corolla acelera a 50 km/h
miCoche:acelerar(30)    --> Toyota Corolla acelera a 80 km/h
miCoche:tocarClaxon()   --> ¡Beep beep!
miCoche:frenar(20)      --> Toyota Corolla frena a 60 km/h

En este ejemplo, usamos __index para implementar herencia. Cuando buscamos un método en un objeto Coche que no existe, Lua busca en la metatabla Coche, y si no lo encuentra allí, busca en Vehiculo gracias a la cadena de metatablas.

Creación de propiedades calculadas

Podemos usar __index y __newindex para crear propiedades que se calculan dinámicamente o que validan datos antes de ser asignados.

function crearPersona(nombre, apellido, anoNacimiento)
  local datosPrivados = {
    nombre = nombre,
    apellido = apellido,
    anoNacimiento = anoNacimiento
  }
  
  local persona = {}
  
  local metatabla = {
    __index = function(t, clave)
      if clave == "nombreCompleto" then
        return datosPrivados.nombre .. " " .. datosPrivados.apellido
      elseif clave == "edad" then
        return os.date("%Y") - datosPrivados.anoNacimiento
      elseif datosPrivados[clave] then
        return datosPrivados[clave]
      end
    end,
    
    __newindex = function(t, clave, valor)
      if clave == "edad" then
        error("No se puede modificar la edad directamente")
      elseif clave == "anoNacimiento" then
        if type(valor) ~= "number" or valor < 1900 or valor > os.date("%Y") then
          error("Año de nacimiento inválido")
        end
        datosPrivados[clave] = valor
      else
        datosPrivados[clave] = valor
      end
    end
  }
  
  setmetatable(persona, metatabla)
  return persona
end

pedro = crearPersona("Pedro", "García", 1990)

print(pedro.nombreCompleto)  --> Pedro García
print(pedro.edad)             --> 35 (o la edad actual)
print(pedro.nombre)           --> Pedro

-- pedro.edad = 40  --> Error: No se puede modificar la edad directamente

Tablas con comportamiento personalizado

Podemos combinar varios metamétodos para crear tablas con comportamientos complejos y útiles.

-- Implementación de un conjunto (set) matemático
function nuevoConjunto(elementos)
  local conjunto = {}
  local datos = {}
  
  -- Inicializar con elementos si se proporcionan
  if elementos then
    for _, elemento in ipairs(elementos) do
      datos[elemento] = true
    end
  end
  
  local metatabla = {
    -- Agregar elemento
    agregar = function(c, elemento)
      datos[elemento] = true
    end,
    
    -- Eliminar elemento
    eliminar = function(c, elemento)
      datos[elemento] = nil
    end,
    
    -- Verificar si contiene un elemento
    contiene = function(c, elemento)
      return datos[elemento] == true
    end,
    
    -- Unión de conjuntos
    __add = function(c1, c2)
      local resultado = nuevoConjunto()
      local mt1 = getmetatable(c1)
      local mt2 = getmetatable(c2)
      
      for elemento in pairs(datos) do
        mt1.agregar(resultado, elemento)
      end
      
      -- Necesitamos acceder a los datos del otro conjunto
      -- Esta es una simplificación; en código real necesitaríamos
      -- un método para iterar sobre los elementos
      return resultado
    end,
    
    -- Tamaño del conjunto
    __len = function(c)
      local contador = 0
      for _ in pairs(datos) do
        contador = contador + 1
      end
      return contador
    end,
    
    -- Representación como cadena
    __tostring = function(c)
      local elementos = {}
      for elemento in pairs(datos) do
        table.insert(elementos, tostring(elemento))
      end
      return "{" .. table.concat(elementos, ", ") .. "}"
    end
  }
  
  setmetatable(conjunto, metatabla)
  return conjunto
end

conjunto1 = nuevoConjunto({1, 2, 3})
conjunto2 = nuevoConjunto({3, 4, 5})

local mt = getmetatable(conjunto1)

mt.agregar(conjunto1, 6)
print(conjunto1)                    --> {1, 2, 3, 6}
print(#conjunto1)                   --> 4
print(mt.contiene(conjunto1, 2))    --> true
print(mt.contiene(conjunto1, 10))   --> false

Casos de uso prácticos

Sistema de configuración con validación

function crearConfiguracion(valoresPorDefecto, validadores)
  local valores = {}
  
  -- Copiar valores por defecto
  for clave, valor in pairs(valoresPorDefecto) do
    valores[clave] = valor
  end
  
  local config = {}
  
  local metatabla = {
    __index = function(t, clave)
      return valores[clave]
    end,
    
    __newindex = function(t, clave, valor)
      -- Verificar si existe un validador
      if validadores[clave] then
        if not validadores[clave](valor) then
          error("Valor inválido para " .. clave)
        end
      end
      valores[clave] = valor
    end
  }
  
  setmetatable(config, metatabla)
  return config
end

-- Definir configuración con validaciones
config = crearConfiguracion(
  {
    puerto = 8080,
    host = "localhost",
    timeout = 30
  },
  {
    puerto = function(v) return type(v) == "number" and v > 0 and v < 65536 end,
    host = function(v) return type(v) == "string" and #v > 0 end,
    timeout = function(v) return type(v) == "number" and v > 0 end
  }
)

print(config.puerto)     --> 8080
config.puerto = 3000     --> OK
-- config.puerto = -1    --> Error: Valor inválido para puerto
-- config.puerto = "abc" --> Error: Valor inválido para puerto

Creación de un logger con metatablas

function crearLogger(nivel)
  local logger = {
    nivelActual = nivel or "INFO",
    niveles = {DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4}
  }
  
  local metatabla = {
    __index = function(t, metodo)
      return function(mensaje)
        local nivelMetodo = metodo:upper()
        if t.niveles[nivelMetodo] and 
           t.niveles[nivelMetodo] >= t.niveles[t.nivelActual] then
          print("[" .. nivelMetodo .. "] " .. os.date("%Y-%m-%d %H:%M:%S") .. " - " .. mensaje)
        end
      end
    end
  }
  
  setmetatable(logger, metatabla)
  return logger
end

log = crearLogger("INFO")

log.debug("Este mensaje no se mostrará")  -- Nivel muy bajo
log.info("Aplicación iniciada")           --> [INFO] 2025-11-02 ... - Aplicación iniciada
log.warn("Advertencia del sistema")       --> [WARN] 2025-11-02 ... - Advertencia del sistema
log.error("Error crítico")                --> [ERROR] 2025-11-02 ... - Error crítico

Consideraciones de rendimiento

Aunque las metatablas son extremadamente potentes, es importante considerar su impacto en el rendimiento:

  1. Búsqueda en metatablas: Cada vez que se invoca un metamétodo, hay una pequeña sobrecarga. Para operaciones críticas en cuanto a rendimiento, considera almacenar valores calculados.

  2. Cadenas de metatablas: Las cadenas largas de metatablas (herencia profunda) pueden afectar el rendimiento. Intenta mantener las jerarquías poco profundas.

  3. Funciones vs. tablas en __index: Usar una tabla en __index es generalmente más rápido que usar una función, ya que evita la llamada a función.

-- Más rápido
metatabla = {__index = tablaDeValores}

-- Más lento (pero más flexible)
metatabla = {__index = function(t, k) return calcularValor(k) end}

Resumen

Las metatablas y metamétodos son herramientas fundamentales en Lua que nos permiten extender y personalizar el comportamiento de las tablas. A través de ellas, podemos implementar programación orientada a objetos, sobrecargar operadores, crear propiedades calculadas y construir abstracciones poderosas que hacen nuestro código más expresivo y mantenible.

Los conceptos clave que hemos cubierto incluyen:

  • Las metatablas modifican el comportamiento de las tablas mediante metamétodos
  • __index y __newindex controlan el acceso y asignación de valores
  • Los operadores aritméticos, relacionales y de concatenación pueden sobrecargarse
  • __tostring y __call permiten comportamientos personalizados
  • Las metatablas son la base para implementar POO en Lua
  • Se pueden crear propiedades calculadas y validaciones personalizadas

Dominar las metatablas te abrirá las puertas a patrones de diseño avanzados y te permitirá crear bibliotecas y frameworks elegantes en Lua. En el siguiente artículo exploraremos la programación orientada a objetos en mayor profundidad, construyendo sobre los conceptos de metatablas que hemos aprendido aquí.