Comparación: por qué __eq es tan tímido y qué pasa si tu orden no es total
El metamétodo de igualdad solo entra en juego entre valores del mismo tipo y solo cuando la identidad ya falló; los de orden, en cambio, se llaman con cualquier par. Añadimos la desaparición del respaldo de __le en Lua 5.4 y las consecuencias reales de definir un orden que no es total.
Los tres metamétodos de comparación parecen los más aburridos del catálogo y son, con diferencia, los que más código roto han producido. La razón es que su semántica no es simétrica con la del resto: la igualdad es extraordinariamente conservadora —solo actúa entre valores del mismo tipo y solo cuando la identidad física ya ha fallado—, mientras que el orden es sorprendentemente permisivo y se deja invocar con cualquier par de cosas. A eso se suma que Lua 5.4 retiró un respaldo que llevaba dos versiones ahí, y que ordenar con una relación mal definida no produce un resultado raro sino un error del intérprete. Vamos a mirar los tres de cerca.
- Saber las tres condiciones que deben cumplirse para que
__eqllegue a ejecutarse. - Distinguir por qué el orden acepta operandos heterogéneos y la igualdad no.
- Recordar que en Lua 5.4
__ledejó de derivarse de__lty hay que definirlo. - Reconocer un orden que no es total y anticipar cómo revienta al ordenar.
La igualdad y sus tres condiciones
Cuando Lua evalúa a == b, antes de pensar en metatablas aplica dos filtros que descartan casi todos los casos.
flowchart TB A[Lua evalua a igual igual b] --> B[Son del mismo tipo basico] B --> C[Si no lo son el resultado es falso sin consultar nada] B --> D[Si lo son compara la identidad primitiva] D --> E[Si son el mismo valor devuelve verdadero] D --> F[Si no y ambos son tablas o ambos userdata busca el metametodo] F --> G[Lo llama y convierte el resultado a booleano] F --> H[Si no hay metametodo devuelve falso]
Las tres condiciones, entonces: los operandos deben ser del mismo tipo, deben ser ambos tablas o ambos userdata completos, y deben no ser ya el mismo valor. Si una falla, no hay llamada. Por eso un número nunca será igual a una tabla por mucho __eq que definas, y por eso comparar una tabla consigo misma devuelve verdadero sin ejecutar tu código.
La restricción al mismo tipo es deliberada y no es una limitación técnica: garantiza que == sea siempre reflexiva, simétrica y barata en el caso común, propiedades de las que dependen las tablas cuando buscan una clave. Y como a ~= b es literalmente not (a == b), no existe __ne: definir la desigualdad por separado permitiría construir valores distintos de sí mismos, y Lua se niega a abrir esa puerta.
Cuando el metamétodo sí se busca, la regla de selección es la misma de la aritmética: primero la metatabla del operando izquierdo, después la del derecho. Como los dos son forzosamente del mismo tipo, el problema de la interoperabilidad se reduce mucho, pero no desaparece: dos tablas de clases distintas pueden acabar comparándose con el __eq de una de ellas. Si tu igualdad no comprueba que el otro operando es de tu tipo, devolverá verdadero para objetos que no tienen nada que ver.
Para saltarse todo el mecanismo y preguntar por la identidad pura existe la comparación cruda de la biblioteca básica, que devuelve verdadero solo si los dos valores son físicamente el mismo. Es la herramienta correcta dentro de un __eq que necesite el caso base, y también la que usa el intérprete internamente antes de decidir si hay algo que preguntar.
Aunque definas __eq de forma que dos tablas distintas sean iguales, siguen siendo dos claves distintas. La indexación usa identidad cruda, no tu metamétodo. Si necesitas que dos valores equivalentes accedan a la misma entrada, tendrás que normalizarlos tú a una clave canónica, típicamente una cadena.
El orden: permisivo, y sin red desde 5.4
Con __lt y __le la historia cambia por completo. Si los dos operandos no son ambos números ni ambas cadenas, Lua busca el metamétodo —primero en el izquierdo, después en el derecho, la misma regla de la aritmética— y lo llama sin exigir que los tipos coincidan. Un __lt puede recibir perfectamente una tabla y un número, y eres tú quien decide si eso significa algo o merece un error.
Solo hay dos metamétodos para cuatro operadores, porque Lua traduce los otros dos por simetría: a > b se evalúa como b < a, y a >= b como b <= a. No existen __gt ni __ge, y no hacen falta.
La novedad que hay que recordar de Lua 5.4 es una retirada. Hasta 5.3, si un valor tenía __lt pero no __le, el intérprete calculaba el menor o igual como la negación del orden invertido. Ese atajo desapareció: si no defines __le, el operador <= sobre tus objetos lanza un error de comparación. La razón es que la equivalencia solo se sostiene en órdenes totales, y el lenguaje no puede saber si el tuyo lo es.
local Version = {}
Version.__index = Version
local function nueva(mayor, menor, parche)
return setmetatable({ mayor, menor, parche }, Version)
end
function Version.__eq(a, b) -- solo se llama entre dos tablas distintas
return a[1] == b[1] and a[2] == b[2] and a[3] == b[3]
end
function Version.__lt(a, b)
for i = 1, 3 do
if a[i] ~= b[i] then return a[i] < b[i] end
end
return false -- iguales: no es estrictamente menor
end
function Version.__le(a, b) -- obligatorio desde 5.4
return not Version.__lt(b, a)
end
local v1, v2 = nueva(1, 4, 0), nueva(1, 10, 2)
print(v1 < v2, v1 <= v2, v2 > v1) -- true true true
print(nueva(2, 0, 0) == nueva(2, 0, 0)) -- true, por __eq
Fíjate en un detalle del ejemplo: comparar componente a componente hace que la versión uno punto cuatro sea anterior a la uno punto diez, cosa que el orden alfabético de las cadenas jamás conseguiría. Ese es justo el tipo de problema que justifica definir un orden propio en lugar de convertir a texto y dejar que el intérprete resuelva.
Solo dos metamétodos, cuatro operadores
El mayor que se traduce en un menor que con los argumentos intercambiados, y el mayor o igual en un menor o igual con el mismo intercambio. No existen __gt ni __ge, así que basta con implementar dos funciones para cubrir las cuatro comparaciones.
El resultado se convierte a booleano
Tu metamétodo puede devolver cualquier valor: el intérprete aplica la regla de veracidad habitual, donde solo el falso y el nulo son falsos. Devolver el número cero significa verdadero, cosa que sorprende a quien llega de otros lenguajes.
El orden acepta tipos distintos
A diferencia de la igualdad, el orden busca metamétodo en cuanto los operandos no son ambos números ni ambas cadenas, sin exigir que coincidan entre sí. Puedes comparar tu tipo con un número si tu metamétodo sabe qué hacer con eso.
Coherencia entre igualdad y orden
Nada obliga a que __eq y __lt cuenten la misma historia, y ese es un error frecuente. Si dos valores son iguales según uno y uno precede al otro según el segundo, cualquier algoritmo que use ambos producirá resultados que dependen del orden de entrada.
Órdenes que no son totales
Un orden es total cuando, para cualquier par de elementos, uno de los dos precede al otro o son equivalentes, y cuando la relación es transitiva. Suena obvio hasta que modelas algo real. La inclusión entre conjuntos es el contraejemplo clásico: dados los conjuntos a con los elementos 1 y 2, y b con los elementos 2 y 3, ni a está contenido en b ni b en a. Son incomparables, que no es lo mismo que iguales.
local Conj = {}
Conj.__index = Conj
function Conj.__le(a, b) -- inclusion: orden PARCIAL
for k in pairs(a.e) do
if not b.e[k] then return false end
end
return true
end
function Conj.__lt(a, b)
return Conj.__le(a, b) and not Conj.__le(b, a)
end
Este código es correcto como modelo de la inclusión y peligroso como orden. Si se lo pasas a table.sort, el algoritmo asume que la relación es estricta y total: usa el resultado para decidir particiones y para saber cuándo parar. Con elementos incomparables, los índices se salen del rango previsto y el intérprete detecta la incoherencia y aborta con invalid order function for sorting. No es un aviso cosmético: sin esa comprobación, la ordenación escribiría fuera de la parte válida de la tabla.
La misma trampa aparece con valores no numéricos comparados por un solo campo cuando hay empates, y aparece de forma nativa con el valor no numérico especial de los flotantes, que devuelve falso frente a cualquier comparación, incluida la de igualdad consigo mismo. Una tabla que contenga uno de esos valores es, por tanto, inordenable con el comparador por defecto, y el síntoma será el mismo mensaje.
Para que un algoritmo de ordenación se comporte, la relación estricta que le pases necesita ser irreflexiva —ningún elemento precede a sí mismo—, asimétrica —si el primero precede al segundo, el segundo no precede al primero— y transitiva —si el primero precede al segundo y el segundo al tercero, el primero precede al tercero—. Además debe ser total en el sentido de que dos elementos cualesquiera son comparables: o uno precede al otro, o son equivalentes. Un orden parcial cumple las tres primeras y falla la cuarta, y eso basta para romperlo todo.
Si tu dominio es genuinamente parcial, la salida correcta no es forzar un orden falso sino ofrecer la relación con nombres explícitos —una función que diga si un conjunto está contenido en otro— y reservar __lt para un criterio distinto y total, como el número de elementos, o no definirlo en absoluto.
Merece la pena preguntarse por qué Lua trata la igualdad con tanta desconfianza cuando deja el orden en tus manos casi sin condiciones, porque la respuesta explica una decisión de arquitectura, no un capricho. La igualdad no es un operador más: es el cimiento sobre el que se apoya la estructura de datos única del lenguaje. Cada vez que escribes una indexación, la tabla necesita decidir si la clave que traes es la misma que una de las que guarda, y esa decisión ocurre millones de veces por segundo, dentro del bucle más caliente del intérprete. Si la igualdad pudiera ser código arbitrario del usuario, cada búsqueda podría disparar una llamada a Lua, podría lanzar un error en mitad de una operación que se supone atómica, podría no ser simétrica, podría dejar de ser transitiva, y toda la tabla dejaría de ser una tabla para convertirse en una lista con búsqueda lineal y semántica impredecible. De ahí las tres condiciones: mismo tipo, ambos tablas o ambos userdata, y solo tras fallar la identidad cruda. Cada una recorta el espacio en el que tu código puede intervenir hasta dejarlo fuera del camino crítico, y la ausencia de __ne cierra la última rendija por la que podría colarse un valor distinto de sí mismo. El orden, en cambio, no sostiene nada: nadie indexa por menor que, ninguna estructura interna depende de él, y por eso el lenguaje te lo entrega sin restricciones de tipo y sin comprobar que sea coherente. La contrapartida llega en cuanto se lo pasas a un algoritmo que sí asume propiedades matemáticas, y entonces el error no dice tu orden es parcial sino función de orden no válida, un mensaje que solo entiende quien ya sabe qué es la totalidad de una relación. La libertad estaba ahí desde el principio; la obligación de que tu relación se comporte también.
__eq solo se llama si los operandos son del mismo tipo, ambos tablas o ambos userdata, y no son ya el mismo valor; su resultado se convierte a booleano y no existe __ne, porque la desigualdad es la negación de la igualdad. El orden es mucho más permisivo: __lt y __le se buscan en cuanto los operandos no son ambos números ni ambas cadenas, sin exigir tipos iguales, y cubren los cuatro operadores porque el mayor que y el mayor o igual se traducen intercambiando argumentos. Desde Lua 5.4, __le ya no se deriva de __lt y hay que definirlo. Y una relación parcial o incoherente no da resultados raros: aborta la ordenación con un error de función de orden no válida.
- Implementa
Versiony comprueba que==entre dos versiones idénticas pero distintas devuelve verdadero, y que comparar una versión con una cadena devuelve falso sin llamar a__eq. - Borra
__ley evalúav1 <= v2en Lua 5.4. Lee el error y explica qué respaldo desapareció. - Comprueba con
printquea > bejecuta__ltcon los argumentos intercambiados. - Implementa
Conjcon la inclusión y ordena una lista de cinco conjuntos incomparables. Reproduce el error de función de orden no válida. - Usa una tabla con
__eqcomo clave de otra tabla, guarda un valor y recupéralo con una tabla equivalente pero distinta. Explica el resultado.