wandres.dev
METATABLAS II · el catálogo completo

Aritmética y concatenación: el catálogo completo y la regla de selección

Los quince metamétodos de las operaciones aritméticas, de bits y de concatenación: qué evento dispara cada uno, cómo decide Lua cuál usar cuando los dos operandos tienen metatabla, por qué no existen metamétodos reflejados al estilo de otros lenguajes, y un tipo vector completo como banco de pruebas.

⏱ 18 min

Una metatabla es la tabla de respuestas que Lua consulta cuando una operación no tiene sentido con los valores que recibe. Toca abrir el catálogo entero, y el bloque más grande es el de la aritmética, los operadores de bits y la concatenación: quince eventos, quince nombres. Aquí no hay magia, hay un algoritmo de dos pasos que puedes recitar de memoria, y una consecuencia incómoda que casi nadie anticipa: como Lua interroga primero al operando de la izquierda, el metamétodo que acaba ejecutándose puede pertenecer a un tipo que no sabe nada del otro. El diseño de tus operadores empieza por asumir esa asimetría, no por ignorarla.

🎯 Al terminar esta lección sabrás
  • Conocer los quince metamétodos de aritmética, bits y concatenación con su evento exacto.
  • Recitar la regla de selección: primero el operando izquierdo, después el derecho.
  • Entender por qué Lua no tiene metamétodos reflejados y qué debe hacer el tuyo en su lugar.
  • Construir un tipo vector cuyos operadores sobreviven al orden en que se escriban.

Cada operador del lenguaje tiene un nombre de evento, y ese nombre es la clave que Lua busca en la metatabla. Son claves normales de una tabla normal: el guion bajo doble es convención tipográfica, no sintaxis privilegiada.

Metamétodo Operador Aridad y nota
__add + binario
__sub - binario
__mul * binario
__div / binario, siempre división flotante
__mod % binario, módulo con signo del divisor
__pow ^ binario, asociativo a la derecha
__unm - unario recibe el operando dos veces
__idiv // binario, división entera hacia abajo
__band & binario, y lógico de bits
__bor | binario, o lógico de bits
__bxor ~ binario o exclusivo de bits
__bnot ~ unario recibe el operando dos veces
__shl << desplazamiento a la izquierda
__shr >> desplazamiento a la derecha
__concat .. binario, concatenación

Los dos unarios, __unm y __bnot, reciben el mismo valor duplicado en sus dos parámetros. No es un descuido de la implementación: uniformar la aridad permite que el intérprete llame a todos los metamétodos binarios con el mismo código, y a ti te permite escribir la función ignorando el segundo argumento.

💡
Los operadores de bits solo hablan de enteros

Sin metatabla, los seis metamétodos de bits solo aceptan enteros o flotantes con parte fraccionaria nula. Un flotante como 3.5 no se convierte: lanza error. Con metatabla, en cambio, tu metamétodo decide qué significa desplazar o negar los bits de tu tipo, y puede darle el sentido que quieras.

Cómo elige Lua

Para una operación binaria, Lua sigue siempre el mismo procedimiento. Primero comprueba si los operandos son válidos de forma nativa: números para la aritmética, enteros para los bits, cadenas o números para la concatenación. Si lo son, ejecuta la operación de máquina y no mira ninguna metatabla. Si no lo son, empieza la búsqueda del metamétodo, y aquí está la regla que hay que grabarse: mira la metatabla del primer operando; si no define el evento, mira la del segundo.

flowchart TB
A[Lua evalua a mas b] --> B[Son ambos operandos validos de forma nativa]
B --> C[Si lo son ejecuta la operacion de maquina]
B --> D[Si no consulta la metatabla del primer operando]
D --> E[Si define el evento lo llama con a y b en ese orden]
D --> F[Si no consulta la metatabla del segundo operando]
F --> E
F --> G[Si tampoco lo define lanza error de aritmetica]

La consecuencia es doble. La primera: el metamétodo recibe siempre los operandos en el orden original, aunque lo hayan encontrado por el lado derecho. En 2 * v, si el vector aporta el __mul, la función se llama con el número como primer argumento. La segunda: no existe nada parecido a un metamétodo reflejado, esa segunda función que otros lenguajes invocan cuando el objeto está a la derecha. Aquí solo hay una función, y su primera responsabilidad es averiguar cuál de los dos argumentos es de su tipo.

⚗️

La validez nativa depende del operador

Para la aritmética, un operando es válido si es un número. Para los operadores de bits, si es un entero o un flotante sin parte fraccionaria. Para la concatenación, si es una cadena o un número. Un mismo valor puede ser válido para una operación e inválido para otra, y por eso el metamétodo se consulta o no según el caso.

🧵

Las cadenas se suman por metamétodo

Que la expresión con la cadena 10 sumada a un número funcione no es una regla especial del intérprete: la biblioteca de cadenas instala una metatabla compartida por todas ellas con los metamétodos aritméticos, y son esos los que intentan la conversión. La coerción es, literalmente, una aplicación de lo que estás aprendiendo.

🚫

No puedes darle metatabla a un número

Desde Lua puro, setmetatable solo acepta tablas. Los números, los booleanos y las funciones comparten una metatabla por tipo que únicamente la biblioteca de depuración puede tocar, y hacerlo afecta a todos los valores de ese tipo del programa entero. No es una vía para extender la aritmética, es una vía para romperla.

🪜

El metamétodo puede devolver lo que quiera

Nada obliga a que la suma de dos vectores devuelva un vector. El resultado del metamétodo es el resultado de la expresión, sin conversión ni comprobación. Esa libertad permite construir tipos que representan operaciones diferidas, árboles de expresión o matrices simbólicas.

Un vector que sobrevive al orden

El ejemplo canónico es un vector de dos componentes. Fíjate en __mul: normaliza el orden antes de operar, que es el patrón que todo metamétodo binario debería seguir.

local Vec = {}
Vec.__index = Vec

local function nuevo(x, y)
  return setmetatable({ x = x, y = y }, Vec)
end

local function esVec(v)
  return getmetatable(v) == Vec
end

function Vec.__add(a, b)
  if not (esVec(a) and esVec(b)) then
    error("solo se suman vectores entre si", 2)
  end
  return nuevo(a.x + b.x, a.y + b.y)
end

function Vec.__unm(v)                 -- Lua pasa v dos veces; usamos el primero
  return nuevo(-v.x, -v.y)
end

function Vec.__mul(a, b)              -- escalar por vector o vector por escalar
  if type(a) == "number" then a, b = b, a end
  if not (esVec(a) and type(b) == "number") then
    error("el producto admite un vector y un escalar", 2)
  end
  return nuevo(a.x * b, a.y * b)
end

function Vec.__concat(a, b)
  return tostring(a) .. tostring(b)
end

function Vec.__tostring(v)
  return string.format("vec[%g, %g]", v.x, v.y)
end

local v = nuevo(1, 2)
print(v + nuevo(3, 4))   -- vec[4, 6]
print(2 * v)             -- vec[2, 4]  el escalar viene primero
print(v * 2)             -- vec[2, 4]  y aqui viene segundo
print(-v)                -- vec[-1, -2]

El truco de a, b = b, a cuesta una línea y elimina toda una clase de errores. Sin él, 2 * v intentaría leer a.x sobre un número y fallaría con un mensaje que no ayuda a nadie.

La trampa de los dos tipos

¿Qué pasa cuando los dos operandos tienen metatabla y ambos definen el mismo evento? La respuesta es tajante: gana el de la izquierda, siempre. Si sumas un vector y un número complejo de otra biblioteca, se ejecuta el __add del vector, que recibe un objeto que no reconoce. No hay negociación, no hay reintento con el otro operando, no hay valor especial que puedas devolver para decir este caso no es mío. O tu metamétodo sabe qué hacer con lo que le llega, o lanza un error decente.

De ahí la disciplina: valida siempre los dos argumentos y falla con error y nivel 2, para que el mensaje señale la línea de quien escribió la operación y no la de tu biblioteca.

El operador no pertenece al tipo, pertenece a la expresión

Aquí está la idea que reorganiza todo lo demás: en Lua un operador no es un método del tipo, es un evento de la expresión, y el tipo solo aporta una posible respuesta. Esa diferencia, que suena a matiz filosófico, tiene consecuencias muy concretas. La primera es que no existe despacho doble: Lua no considera el par de tipos para elegir la implementación, considera un operando, y si falla el otro, en ese orden y sin dar marcha atrás. La segunda es que tu función es la única implementación posible de esa operación para tu tipo, esté tu valor a la izquierda o a la derecha, lo cual convierte la normalización de argumentos en parte del contrato y no en una comodidad. La tercera, y la más profunda, es que la interoperabilidad entre bibliotecas queda fuera del alcance del lenguaje: si dos módulos definen tipos numéricos distintos y alguien los mezcla, el ganador lo decide el orden en que se escribieron en el código fuente, no una jerarquía de conversiones ni un sistema de tipos que arbitre. Otros lenguajes intentan resolverlo con metamétodos reflejados, con conversiones implícitas o con una tabla de coerciones, y pagan por ello complejidad, sorpresas y reglas que hay que memorizar. Lua elige lo contrario: una regla de dos líneas que cabe en la cabeza, y la responsabilidad entera trasladada a quien escribe el metamétodo. Es el mismo trato que el lenguaje ofrece en todas partes —menos mecanismo, más criterio—, y por eso el buen código Lua valida sus operandos con obsesión: porque el intérprete no lo va a hacer por ti, y porque el error que no lances tú se manifestará mucho más lejos, disfrazado de índice nulo dentro de una biblioteca ajena.

📝
Lo esencial

Quince eventos cubren aritmética, bits y concatenación. Lua solo los busca cuando la operación nativa no es aplicable, consultando primero la metatabla del operando izquierdo y después la del derecho, y llamando siempre con los operandos en su orden original. Los unarios __unm y __bnot reciben el valor duplicado. No hay metamétodos reflejados: una sola función atiende ambos lados, así que debe normalizar el orden y validar los dos argumentos.

⚔️ Somete tus operadores a la asimetría
  1. Escribe el tipo Vec completo y comprueba con print que 2 * v y v * 2 dan el mismo resultado. Quita el intercambio de __mul y observa el error.
  2. Define un segundo tipo, Complejo, también con __add. Evalúa v + c y luego c + v, y explica cuál de los dos metamétodos se ejecutó en cada caso.
  3. Añade __idiv a Vec con la semántica que te parezca defendible y justifica por escrito por qué esa y no otra.
  4. Sin ejecutar nada, predice qué ocurre con v .. "hola" y con "hola" .. v. Después compruébalo.
  5. Comprueba que __unm recibe el mismo valor dos veces imprimiendo ambos parámetros dentro del metamétodo.