← Volver al listado de tecnologías

Capítulo 12: Sobrecarga de Operadores

Por: Artiko
luaoperator-overloadingmetamethodsarithmetic

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:

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:

  1. Verifica si a tiene metatabla con el metamétodo correspondiente (__add)
  2. Si no, verifica si b tiene metatabla con el metamétodo
  3. 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:

  1. El comportamiento es matemáticamente obvio (vectores, matrices, números complejos)
  2. Sigue las convenciones matemáticas estándar
  3. No tiene efectos secundarios
  4. 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:

Esto es algo BUENO. La simplicidad de Lua previene el 90% de los abusos que veo en código C++.

Mi filosofía personal:

  1. Si no es matemática, no sobrecargues operadores
  2. Si requiere documentación extensa, usa métodos nombrados
  3. Si tu operador + no es conmutativo, reconsidéralo
  4. 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:

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:

  1. Operadores Aritméticos: __add, __sub, __mul, __div, __mod, __pow, __unm
  2. Operadores de Comparación: __eq, __lt, __le (los demás se derivan automáticamente)
  3. Concatenación: __concat (úsalo con moderación)
  4. Reglas de Lookup: Lua busca primero en el operando izquierdo, luego en el derecho
  5. Anti-Patterns: Cuándo NO usar sobrecarga de operadores
  6. Precedencia y Evaluación: Cómo Lua evalúa expresiones complejas
  7. Á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!