Capítulo 12: Sobrecarga de Operadores
Capítulo 12: Sobrecarga de Operadores
La sobrecarga de operadores es una de las características más elegantes (y peligrosas) que ofrecen las metatablas en Lua. Permite que tus tipos personalizados se comporten como tipos nativos, haciendo que vector1 + vector2 sea tan natural como 2 + 3. Pero como dice Uncle Bob: “Con gran poder viene gran responsabilidad”.
En este capítulo aprenderás a sobrecargar operadores de forma idiomática, entenderás cuándo es apropiado hacerlo (y cuándo definitivamente NO), y construirás una librería completa de álgebra lineal que rivaliza con las implementaciones profesionales.
12.1. Operadores Aritméticos
Lua permite sobrecargar todos los operadores aritméticos básicos mediante metamétodos específicos.
12.1.1. Suma y Resta: __add y __sub
-- Vector 2D simple
local Vector2D = {}
Vector2D.__index = Vector2D
function Vector2D.new(x, y)
local self = setmetatable({}, Vector2D)
self.x = x or 0
self.y = y or 0
return self
end
-- Sobrecarga del operador +
function Vector2D.__add(a, b)
-- Manejar caso cuando uno de los operandos es un número
if type(a) == "number" then
return Vector2D.new(a + b.x, a + b.y)
elseif type(b) == "number" then
return Vector2D.new(a.x + b, a.y + b)
else
return Vector2D.new(a.x + b.x, a.y + b.y)
end
end
-- Sobrecarga del operador -
function Vector2D.__sub(a, b)
if type(a) == "number" then
return Vector2D.new(a - b.x, a - b.y)
elseif type(b) == "number" then
return Vector2D.new(a.x - b, a.y - b)
else
return Vector2D.new(a.x - b.x, a.y - b.y)
end
end
function Vector2D:__tostring()
return string.format("Vector2D(%.2f, %.2f)", self.x, self.y)
end
-- Uso
local v1 = Vector2D.new(3, 4)
local v2 = Vector2D.new(1, 2)
print(v1 + v2) -- Vector2D(4.00, 6.00)
print(v1 - v2) -- Vector2D(2.00, 2.00)
print(v1 + 5) -- Vector2D(8.00, 9.00)
print(10 - v2) -- Vector2D(9.00, 8.00)
12.1.2. Multiplicación y División: __mul y __div
-- Continuando con Vector2D...
-- Sobrecarga del operador *
function Vector2D.__mul(a, b)
-- Multiplicación escalar
if type(a) == "number" then
return Vector2D.new(a * b.x, a * b.y)
elseif type(b) == "number" then
return Vector2D.new(a.x * b, a.y * b)
else
-- Producto componente a componente (Hadamard)
return Vector2D.new(a.x * b.x, a.y * b.y)
end
end
-- Sobrecarga del operador /
function Vector2D.__div(a, b)
if type(a) == "number" then
return Vector2D.new(a / b.x, a / b.y)
elseif type(b) == "number" then
return Vector2D.new(a.x / b, a.y / b)
else
return Vector2D.new(a.x / b.x, a.y / b.y)
end
end
-- Uso
local v = Vector2D.new(6, 8)
print(v * 2) -- Vector2D(12.00, 16.00)
print(3 * v) -- Vector2D(18.00, 24.00)
print(v / 2) -- Vector2D(3.00, 4.00)
local v3 = Vector2D.new(2, 4)
print(v * v3) -- Vector2D(12.00, 32.00) -- Producto Hadamard
12.1.3. Módulo, Potencia y Negación Unaria: __mod, __pow, __unm
-- Sobrecarga del operador % (módulo)
function Vector2D.__mod(a, b)
if type(b) == "number" then
return Vector2D.new(a.x % b, a.y % b)
else
return Vector2D.new(a.x % b.x, a.y % b.y)
end
end
-- Sobrecarga del operador ^ (potencia)
function Vector2D.__pow(a, b)
if type(b) == "number" then
return Vector2D.new(a.x ^ b, a.y ^ b)
else
return Vector2D.new(a.x ^ b.x, a.y ^ b.y)
end
end
-- Sobrecarga del operador unario - (negación)
function Vector2D.__unm(v)
return Vector2D.new(-v.x, -v.y)
end
-- Uso
local v = Vector2D.new(7, 9)
print(v % 3) -- Vector2D(1.00, 0.00)
print(v ^ 2) -- Vector2D(49.00, 81.00)
print(-v) -- Vector2D(-7.00, -9.00)
12.1.4. Métodos de Utilidad para Vectores
-- Magnitud (longitud) del vector
function Vector2D:magnitude()
return math.sqrt(self.x * self.x + self.y * self.y)
end
-- Normalización (vector unitario)
function Vector2D:normalize()
local mag = self:magnitude()
if mag > 0 then
return Vector2D.new(self.x / mag, self.y / mag)
end
return Vector2D.new(0, 0)
end
-- Producto punto (dot product)
function Vector2D:dot(other)
return self.x * other.x + self.y * other.y
end
-- Distancia entre dos vectores
function Vector2D:distance(other)
return (self - other):magnitude()
end
-- Uso
local v1 = Vector2D.new(3, 4)
print("Magnitud:", v1:magnitude()) -- 5.0
local v_norm = v1:normalize()
print("Normalizado:", v_norm) -- Vector2D(0.60, 0.80)
local v2 = Vector2D.new(1, 0)
print("Dot product:", v1:dot(v2)) -- 3.0
12.2. Operadores de Comparación
Los operadores de comparación son más limitados que los aritméticos, pero igualmente poderosos.
12.2.1. Igualdad: __eq
-- Sobrecarga del operador == (igualdad)
function Vector2D.__eq(a, b)
return a.x == b.x and a.y == b.y
end
-- Uso
local v1 = Vector2D.new(3, 4)
local v2 = Vector2D.new(3, 4)
local v3 = Vector2D.new(5, 6)
print(v1 == v2) -- true
print(v1 == v3) -- false
IMPORTANTE: El operador ~= (no igual) se deriva automáticamente de __eq. Si a == b es false, entonces a ~= b es true.
12.2.2. Menor que y Menor o Igual: __lt y __le
-- Sobrecarga del operador < (menor que)
-- Compara por magnitud
function Vector2D.__lt(a, b)
return a:magnitude() < b:magnitude()
end
-- Sobrecarga del operador <= (menor o igual)
function Vector2D.__le(a, b)
return a:magnitude() <= b:magnitude()
end
-- Uso
local v1 = Vector2D.new(3, 4) -- magnitud = 5
local v2 = Vector2D.new(5, 12) -- magnitud = 13
local v3 = Vector2D.new(3, 4) -- magnitud = 5
print(v1 < v2) -- true
print(v2 < v1) -- false
print(v1 <= v3) -- true
IMPORTANTE: Los operadores > y >= se derivan automáticamente:
a > bes equivalente ab < aa >= bes equivalente ab <= a
12.2.3. Comparación de Números Complejos
local Complex = {}
Complex.__index = Complex
function Complex.new(real, imag)
local self = setmetatable({}, Complex)
self.real = real or 0
self.imag = imag or 0
return self
end
function Complex:magnitude()
return math.sqrt(self.real^2 + self.imag^2)
end
-- Igualdad exacta
function Complex.__eq(a, b)
return a.real == b.real and a.imag == b.imag
end
-- Comparación por magnitud
function Complex.__lt(a, b)
return a:magnitude() < b:magnitude()
end
function Complex.__le(a, b)
return a:magnitude() <= b:magnitude()
end
function Complex.__tostring(c)
if c.imag >= 0 then
return string.format("%.2f + %.2fi", c.real, c.imag)
else
return string.format("%.2f - %.2fi", c.real, -c.imag)
end
end
-- Uso
local c1 = Complex.new(3, 4)
local c2 = Complex.new(5, 12)
print(c1) -- 3.00 + 4.00i
print(c1 < c2) -- true (5 < 13)
12.3. Operador de Concatenación: __concat
El operador .. puede sobrecargarse para crear representaciones personalizadas.
-- Sobrecarga del operador .. (concatenación)
function Vector2D.__concat(a, b)
return tostring(a) .. " -> " .. tostring(b)
end
-- Uso
local v1 = Vector2D.new(0, 0)
local v2 = Vector2D.new(3, 4)
print(v1 .. v2) -- Vector2D(0.00, 0.00) -> Vector2D(3.00, 4.00)
ADVERTENCIA: El operador de concatenación es el que tiene más potencial de abuso. Úsalo con moderación.
-- MAL: Comportamiento confuso
function Vector2D.__concat(a, b)
return a + b -- ¿Por qué .. haría suma?
end
-- BIEN: Comportamiento intuitivo
function Vector2D.__concat(a, b)
return tostring(a) .. ", " .. tostring(b)
end
12.4. Reglas de Lookup de Metamétodos
Cuando Lua encuentra una operación binaria como a + b, sigue este proceso:
- Verifica si
atiene metatabla con el metamétodo correspondiente (__add) - Si no, verifica si
btiene metatabla con el metamétodo - Si ninguno tiene el metamétodo, lanza un error
local Vector2D = {}
Vector2D.__index = Vector2D
function Vector2D.new(x, y)
return setmetatable({x = x, y = y}, Vector2D)
end
function Vector2D.__add(a, b)
print("Llamando __add de Vector2D")
if type(a) == "number" then
return Vector2D.new(a + b.x, a + b.y)
elseif type(b) == "number" then
return Vector2D.new(a.x + b, a.y + b)
else
return Vector2D.new(a.x + b.x, a.y + b.y)
end
end
local v1 = Vector2D.new(1, 2)
local v2 = Vector2D.new(3, 4)
-- Caso 1: Ambos tienen la misma metatabla
print(v1 + v2) -- Llamando __add de Vector2D
-- Caso 2: Primer operando es número (sin metatabla)
-- Lua busca en el segundo operando
print(5 + v1) -- Llamando __add de Vector2D
-- Caso 3: Segundo operando es número
print(v1 + 10) -- Llamando __add de Vector2D
12.4.1. Precedencia de Metatablas
local TypeA = {}
TypeA.__index = TypeA
function TypeA.new(value)
return setmetatable({value = value}, TypeA)
end
function TypeA.__add(a, b)
print("Usando __add de TypeA")
return TypeA.new(a.value + (b.value or b))
end
local TypeB = {}
TypeB.__index = TypeB
function TypeB.new(value)
return setmetatable({value = value}, TypeB)
end
function TypeB.__add(a, b)
print("Usando __add de TypeB")
return TypeB.new(a.value + (b.value or b))
end
local a = TypeA.new(5)
local b = TypeB.new(10)
-- El primer operando tiene precedencia
print((a + b).value) -- Usando __add de TypeA -> 15
print((b + a).value) -- Usando __add de TypeB -> 15
12.5. Cuándo NO Usar Sobrecarga de Operadores
La sobrecarga de operadores puede hacer tu código más elegante, pero también puede destruir su legibilidad. Aquí están las reglas de oro:
12.5.1. Anti-Pattern 1: Operadores con Comportamiento No Intuitivo
-- ❌ MAL: El operador + no debería hacer esto
local Logger = {}
function Logger.__add(a, b)
io.write(tostring(b) .. "\n") -- ¿Por qué + imprime?
return a
end
-- ✅ BIEN: Método explícito
function Logger:log(message)
io.write(tostring(message) .. "\n")
end
12.5.2. Anti-Pattern 2: Operadores con Efectos Secundarios Ocultos
-- ❌ MAL: Efectos secundarios inesperados
local Counter = {}
Counter.__index = Counter
Counter.total = 0
function Counter.new(value)
return setmetatable({value = value}, Counter)
end
function Counter.__add(a, b)
Counter.total = Counter.total + 1 -- Efecto secundario global
return Counter.new(a.value + b.value)
end
-- ✅ BIEN: Operación pura
function Counter.__add(a, b)
return Counter.new(a.value + b.value)
end
function Counter.incrementTotal()
Counter.total = Counter.total + 1
end
12.5.3. Anti-Pattern 3: Operadores Demasiado Genéricos
-- ❌ MAL: ¿Qué hace realmente +?
local DataSet = {}
function DataSet.__add(a, b)
-- ¿Union? ¿Merge? ¿Concatenación?
-- ¡Imposible saberlo sin leer el código!
end
-- ✅ BIEN: Métodos explícitos
function DataSet:union(other)
-- Implementación clara
end
function DataSet:merge(other)
-- Implementación clara
end
12.5.4. Regla de Oro
Sobrecarga operadores solo cuando:
- El comportamiento es matemáticamente obvio (vectores, matrices, números complejos)
- Sigue las convenciones matemáticas estándar
- No tiene efectos secundarios
- Mejora la legibilidad en lugar de oscurecerla
12.6. DEEP DIVE: Orden de Evaluación de Operadores
Lua evalúa las expresiones con operadores sobrecargados siguiendo reglas específicas de precedencia.
12.6.1. Precedencia y Asociatividad
local v1 = Vector2D.new(1, 2)
local v2 = Vector2D.new(3, 4)
local v3 = Vector2D.new(5, 6)
-- Suma es asociativa a la izquierda
-- v1 + v2 + v3 se evalúa como (v1 + v2) + v3
local result = v1 + v2 + v3
print(result) -- Vector2D(9.00, 12.00)
-- Multiplicación tiene mayor precedencia que suma
-- v1 + v2 * 2 se evalúa como v1 + (v2 * 2)
local result2 = v1 + v2 * 2
print(result2) -- Vector2D(7.00, 10.00)
12.6.2. Optimización de Expresiones Complejas
-- Expresión compleja
local v1 = Vector2D.new(1, 0)
local v2 = Vector2D.new(0, 1)
local v3 = Vector2D.new(1, 1)
-- Cada operación crea un nuevo objeto
local result = v1 * 2 + v2 * 3 - v3 / 2
-- Crea 5 objetos temporales: (v1*2), (v2*3), (v1*2 + v2*3), (v3/2), resultado final
-- Versión optimizada con método chainable
function Vector2D:scale(s)
self.x = self.x * s
self.y = self.y * s
return self
end
function Vector2D:add(other)
self.x = self.x + other.x
self.y = self.y + other.y
return self
end
-- Solo modifica objetos existentes (mutable)
local result_opt = Vector2D.new(0, 0)
:add(v1:scale(2))
:add(v2:scale(3))
:add((v3 / 2):scale(-1))
12.7. SOAPBOX: Lua vs C++ Operator Overloading
Vengo del mundo de C++ donde la sobrecarga de operadores puede convertirse en un infierno de ambigüedad. Lua es refrescantemente simple en comparación:
C++ te deja sobrecargar casi todo:
// C++: Puedes sobrecargar hasta el operador coma
Vector operator,(const Vector& a, const Vector& b) {
// ¿Qué diablos hace esto?
}
Lua es minimalista:
- Solo operadores matemáticos y de comparación
- No puedes sobrecargar
and,or,not - No puedes sobrecargar el acceso a campos (excepto con
__index) - No puedes sobrecargar la asignación
Esto es algo BUENO. La simplicidad de Lua previene el 90% de los abusos que veo en código C++.
Mi filosofía personal:
- Si no es matemática, no sobrecargues operadores
- Si requiere documentación extensa, usa métodos nombrados
- Si tu operador
+no es conmutativo, reconsidéralo - Si tiene efectos secundarios, definitivamente NO
12.8. Caso Práctico: Librería de Matrices y Vectores
Vamos a construir una librería completa de álgebra lineal con vectores y matrices.
12.8.1. Clase Matrix
local Matrix = {}
Matrix.__index = Matrix
function Matrix.new(rows, cols, data)
local self = setmetatable({}, Matrix)
self.rows = rows
self.cols = cols
self.data = data or {}
-- Inicializar con ceros si no hay data
if not data then
for i = 1, rows do
self.data[i] = {}
for j = 1, cols do
self.data[i][j] = 0
end
end
end
return self
end
-- Constructor desde array
function Matrix.fromArray(arr)
local rows = #arr
local cols = #arr[1]
return Matrix.new(rows, cols, arr)
end
-- Matriz identidad
function Matrix.identity(n)
local m = Matrix.new(n, n)
for i = 1, n do
m.data[i][i] = 1
end
return m
end
-- Obtener/establecer elemento
function Matrix:get(i, j)
return self.data[i][j]
end
function Matrix:set(i, j, value)
self.data[i][j] = value
end
-- Sobrecarga de suma
function Matrix.__add(a, b)
if a.rows ~= b.rows or a.cols ~= b.cols then
error("Las matrices deben tener las mismas dimensiones")
end
local result = Matrix.new(a.rows, a.cols)
for i = 1, a.rows do
for j = 1, a.cols do
result:set(i, j, a:get(i, j) + b:get(i, j))
end
end
return result
end
-- Sobrecarga de resta
function Matrix.__sub(a, b)
if a.rows ~= b.rows or a.cols ~= b.cols then
error("Las matrices deben tener las mismas dimensiones")
end
local result = Matrix.new(a.rows, a.cols)
for i = 1, a.rows do
for j = 1, a.cols do
result:set(i, j, a:get(i, j) - b:get(i, j))
end
end
return result
end
-- Sobrecarga de multiplicación
function Matrix.__mul(a, b)
-- Multiplicación escalar
if type(a) == "number" then
local result = Matrix.new(b.rows, b.cols)
for i = 1, b.rows do
for j = 1, b.cols do
result:set(i, j, a * b:get(i, j))
end
end
return result
elseif type(b) == "number" then
local result = Matrix.new(a.rows, a.cols)
for i = 1, a.rows do
for j = 1, a.cols do
result:set(i, j, a:get(i, j) * b)
end
end
return result
end
-- Multiplicación de matrices
if a.cols ~= b.rows then
error("Dimensiones incompatibles para multiplicación")
end
local result = Matrix.new(a.rows, b.cols)
for i = 1, a.rows do
for j = 1, b.cols do
local sum = 0
for k = 1, a.cols do
sum = sum + a:get(i, k) * b:get(k, j)
end
result:set(i, j, sum)
end
end
return result
end
-- Negación unaria
function Matrix.__unm(m)
return -1 * m
end
-- Igualdad
function Matrix.__eq(a, b)
if a.rows ~= b.rows or a.cols ~= b.cols then
return false
end
for i = 1, a.rows do
for j = 1, a.cols do
if a:get(i, j) ~= b:get(i, j) then
return false
end
end
end
return true
end
-- Representación como string
function Matrix:__tostring()
local lines = {}
for i = 1, self.rows do
local row = {}
for j = 1, self.cols do
table.insert(row, string.format("%6.2f", self:get(i, j)))
end
table.insert(lines, "[ " .. table.concat(row, " ") .. " ]")
end
return table.concat(lines, "\n")
end
-- Transpuesta
function Matrix:transpose()
local result = Matrix.new(self.cols, self.rows)
for i = 1, self.rows do
for j = 1, self.cols do
result:set(j, i, self:get(i, j))
end
end
return result
end
12.8.2. Uso de la Librería de Matrices
-- Crear matrices
local m1 = Matrix.fromArray({
{1, 2, 3},
{4, 5, 6}
})
local m2 = Matrix.fromArray({
{7, 8, 9},
{10, 11, 12}
})
-- Suma de matrices
print("m1 + m2:")
print(m1 + m2)
--[[
[ 8.00 10.00 12.00 ]
[ 14.00 16.00 18.00 ]
]]
-- Multiplicación escalar
print("\n2 * m1:")
print(2 * m1)
--[[
[ 2.00 4.00 6.00 ]
[ 8.00 10.00 12.00 ]
]]
-- Multiplicación de matrices
local m3 = Matrix.fromArray({
{1, 2},
{3, 4},
{5, 6}
})
print("\nm1 * m3:")
print(m1 * m3)
--[[
[ 22.00 28.00 ]
[ 49.00 64.00 ]
]]
-- Matriz identidad
local identity = Matrix.identity(3)
print("\nIdentidad 3x3:")
print(identity)
-- Transpuesta
print("\nTranspuesta de m1:")
print(m1:transpose())
12.8.3. Integración con Vector2D
-- Convertir vector a matriz columna
function Vector2D:toMatrix()
return Matrix.fromArray({
{self.x},
{self.y}
})
end
-- Crear vector desde matriz
function Matrix:toVector2D()
if self.cols ~= 1 or self.rows ~= 2 then
error("La matriz debe ser 2x1")
end
return Vector2D.new(self:get(1, 1), self:get(2, 1))
end
-- Transformación de vector por matriz
local rotation90 = Matrix.fromArray({
{0, -1},
{1, 0}
})
local v = Vector2D.new(1, 0)
local v_matrix = v:toMatrix()
local rotated_matrix = rotation90 * v_matrix
local rotated_vector = rotated_matrix:toVector2D()
print("Vector original:", v) -- Vector2D(1.00, 0.00)
print("Vector rotado 90°:", rotated_vector) -- Vector2D(0.00, 1.00)
12.9. Ejercicios
Ejercicio 1: Números Racionales
Implementa una clase Rational que represente números racionales (fracciones) con sobrecarga completa de operadores:
local Rational = {}
-- Tu implementación aquí
-- Debe soportar:
local r1 = Rational.new(1, 2) -- 1/2
local r2 = Rational.new(1, 3) -- 1/3
print(r1 + r2) -- 5/6
print(r1 * r2) -- 1/6
print(r1 < r2) -- false
print(r1 == Rational.new(2, 4)) -- true (simplificado)
Requisitos:
- Simplificar fracciones automáticamente (usar GCD)
- Soportar todos los operadores aritméticos
- Soportar comparaciones
- Manejar división por cero
Ejercicio 2: Vector3D con Producto Cruz
Extiende Vector2D a 3 dimensiones y agrega el producto cruz:
local Vector3D = {}
-- Tu implementación aquí
-- Debe soportar:
local v1 = Vector3D.new(1, 0, 0)
local v2 = Vector3D.new(0, 1, 0)
print(v1:cross(v2)) -- Vector3D(0, 0, 1)
print(v1:dot(v2)) -- 0
-- Bonus: Sobrecarga el operador ^ para producto cruz
print(v1 ^ v2) -- Vector3D(0, 0, 1)
Ejercicio 3: Sistema de Unidades Físicas
Crea un sistema de tipos para magnitudes físicas que prevenga errores de unidades:
local Distance = {} -- metros
local Time = {} -- segundos
local Velocity = {} -- metros/segundo
-- Debe prevenir:
local d = Distance.new(100) -- 100 metros
local t = Time.new(10) -- 10 segundos
local v = d / t -- Velocity(10 m/s) ✓
-- local invalid = d + t -- ERROR: No puedes sumar metros y segundos ✓
-- Debe soportar:
local d2 = Distance.new(50)
local total = d + d2 -- Distance(150) ✓
local accel = v / t -- Acceleration(1 m/s²) ✓
Resumen del Capítulo
En este capítulo has aprendido:
- Operadores Aritméticos:
__add,__sub,__mul,__div,__mod,__pow,__unm - Operadores de Comparación:
__eq,__lt,__le(los demás se derivan automáticamente) - Concatenación:
__concat(úsalo con moderación) - Reglas de Lookup: Lua busca primero en el operando izquierdo, luego en el derecho
- Anti-Patterns: Cuándo NO usar sobrecarga de operadores
- Precedencia y Evaluación: Cómo Lua evalúa expresiones complejas
- Álgebra Lineal: Implementación completa de vectores y matrices
La sobrecarga de operadores es una herramienta poderosa que, usada correctamente, hace que tu código sea elegante y expresivo. Pero recuerda: con gran poder viene gran responsabilidad. Úsala solo cuando mejore genuinamente la legibilidad y siga convenciones matemáticas estándar.
En el próximo capítulo exploraremos Coroutines, una de las características más distintivas de Lua que te permite escribir código concurrente de forma elegante y sin los dolores de cabeza del multi-threading tradicional.
¡Nos vemos en el Capítulo 13!