Igualdad y orden: por qué el float solo es PartialOrd
PartialEq y Eq gobiernan la comparación con doble igual; PartialOrd y Ord gobiernan el orden. La familia Partial admite huecos y por eso el f64, con su NaN, solo llega a Partial: el sistema de tipos codifica honestamente que los flotantes no forman ni una igualdad ni un orden totales.
Comparar con == y ordenar con < parecen operaciones triviales hasta que aparece el NaN, el “no es un número” de los flotantes, que no es igual ni a sí mismo. Rust se toma esa anomalía en serio y reparte la comparación en cuatro traits en dos niveles: PartialEq y PartialOrd, que admiten huecos, y sus sellos Eq y Ord, que prometen que no hay ninguno. El resultado es que f64 puede compararse, pero honestamente se queda en la familia Partial. Ese detalle, que parece pedante, es el sistema de tipos negándose a mentir sobre las matemáticas.
- Comparar con
==mediantePartialEqy entender qué añade el selloEq. - Ordenar con
<,>mediantePartialOrdy qué garantiza el selloOrd. - Explicar por qué
f64esPartialEqyPartialOrdpero nuncaEqniOrd. - Reconocer que
derivecompara campo a campo en orden de declaración.
Igualdad: PartialEq y el sello Eq
PartialEq habilita == y !=. Se deriva y compara campo a campo:
#[derive(PartialEq)]
struct Version {
mayor: u32,
menor: u32,
}
fn main() {
let a = Version { mayor: 1, menor: 4 };
let b = Version { mayor: 1, menor: 4 };
assert!(a == b); // igual campo a campo
}
Eq es un trait marcador: no añade ni un método sobre PartialEq. Lo que aporta es una promesa matemática: que la igualdad es reflexiva, es decir, que a == a es verdad para todo valor a. Con PartialEq a secas tienes solo simetría (a == b implica b == a) y transitividad; Eq añade la reflexividad que faltaba para tener una relación de equivalencia completa.
Orden: PartialOrd y el sello Ord
PartialOrd habilita <, >, <= y >=. Su método clave es partial_cmp, que devuelve Option<Ordering>: un Some(Less | Equal | Greater) cuando los valores son comparables, o None cuando no lo son. Ese Option es la puerta por la que entran los huecos.
Ord es el sello que promete que no hay huecos: su método cmp devuelve un Ordering directo, sin Option, porque garantiza un orden total: dos valores cualesquiera siempre son comparables y hay un ganador (o son iguales). Ord requiere Eq y PartialOrd, porque un orden total presupone una igualdad total.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Prioridad(u8);
fn main() {
let mut v = vec![Prioridad(3), Prioridad(1), Prioridad(2)];
v.sort(); // sort() EXIGE Ord: necesita un orden total
}
flowchart TD PE[PartialEq da doble igual] -->|sello reflexividad| EQ[Eq igualdad total] PO[PartialOrd da menor y mayor con Option] -->|sello sin huecos| OR[Ord orden total] EQ --> OR PO -.requiere.-> PE OR --> USO[Claves de BTreeMap y metodo sort] EQ --> USH[Claves de HashMap con Hash] style PE fill:#89b4fa,color:#11111b style PO fill:#89b4fa,color:#11111b style EQ fill:#a6e3a1,color:#11111b style OR fill:#a6e3a1,color:#11111b
Por qué el float solo es PartialOrd
Los flotantes de la norma IEEE-754 tienen un valor especial, NaN (not a number), que aparece en operaciones como 0.0 / 0.0. La norma dicta que NaN no es igual a nada, ni siquiera a sí mismo:
fn main() {
let nan = f64::NAN;
assert!(nan != nan); // NaN nunca es igual a si mismo
assert!(!(1.0 < nan)); // toda comparacion con NaN es falsa
assert!(!(1.0 > nan)); // ni menor, ni mayor, ni igual: incomparable
assert_eq!(nan.partial_cmp(&1.0), None); // partial_cmp lo confiesa con None
}
Esto rompe las dos promesas de golpe. La reflexividad de Eq falla, porque nan == nan es falso: por eso f64 implementa PartialEq pero no Eq. Y la totalidad de Ord falla, porque NaN es incomparable con cualquier número —partial_cmp devuelve None—: por eso f64 implementa PartialOrd pero no Ord.
La consecuencia es concreta y a veces frustrante: no puedes usar un f64 como clave de un HashMap (necesita Eq) ni ordenar un Vec<f64> con .sort() (necesita Ord). Rust no te lo prohíbe por capricho: te está impidiendo construir una tabla hash sobre una igualdad rota o un orden sobre una relación con agujeros, dos bugs silenciosos que casi cualquier otro lenguaje te dejaría cometer.
Cuando sabes que no hay NaN, o quieres imponer una posición a los que haya, usa .sort_by(|a, b| a.total_cmp(b)). El método f64::total_cmp implementa el totalOrder de IEEE-754: coloca NaN en un extremo y devuelve un Ordering completo, dándote a mano el orden total que el tipo no promete por defecto. La responsabilidad de que ese orden tenga sentido pasa a ser tuya.
Las leyes que tú garantizas
Eq y Ord no añaden métodos: son marcadores de leyes. Cuando derivas, la mecánica campo a campo las respeta por construcción. Pero si implementas PartialEq u Ord a mano, eres tú quien promete que se cumplen —reflexividad, simetría, transitividad, totalidad— y nadie lo verifica. Romperlas no es undefined behavior, pero sí un bug lógico venenoso: un Ord inconsistente hace que un BTreeMap pierda elementos o entregue resultados absurdos, y un PartialEq no simétrico corrompe cualquier búsqueda.
Un detalle del derive que hay que interiorizar: compara campo a campo en el orden de declaración. El primer campo manda; los siguientes solo desempatan. Reordenar los campos de un struct con #[derive(Ord)] cambia su orden, aunque los datos sean los mismos.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
struct Semver {
mayor: u32, // se compara primero
menor: u32, // solo desempata si 'mayor' coincide
parche: u32, // solo desempata si 'mayor' y 'menor' coinciden
}
La misma regla mecánica rige los enum, con un giro: derive los ordena por el orden de declaración de las variantes, y solo dentro de una misma variante compara sus datos. Así, un enum Prioridad { Baja, Media, Alta } cumple que Baja es menor que Media, y esta menor que Alta, simplemente porque Baja se declaró primero. Reordenar las variantes cambia el orden resultante igual que reordenar los campos de un struct. Es comodísimo para modelar escalas —niveles de log, rangos, severidades— sin escribir números a mano, pero recuerda que el orden vive en la posición, no en un valor explícito.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Severidad {
Info, // la menor
Aviso,
Error, // la mayor
}La familia PartialEq / Eq / PartialOrd / Ord no es una colección de traits de conveniencia: es una jerarquía algebraica codificada en el sistema de tipos. PartialEq es una relación de equivalencia parcial; Eq le añade la reflexividad que la convierte en equivalencia total. PartialOrd es un orden parcial —admite pares incomparables—; Ord le añade la totalidad. Rust parte la jerarquía justo por donde las matemáticas exigen que se parta, y la razón profunda es la honestidad. Los flotantes IEEE-754 genuinamente no forman ni una igualdad ni un orden totales, porque el NaN viola la reflexividad y la comparabilidad; son objetos matemáticos con huecos reales. Casi todos los lenguajes fingen que no: te dan == sobre floats como si fuera una equivalencia, y luego NaN == NaN es falso y tu tabla hash pierde entradas, o tu ordenación entra en bucle, sin un solo aviso. Rust se niega a la ficción. Al tipar f64 como PartialOrd pero no Ord, el lenguaje te dice la verdad —“esto tiene huecos”— en la firma misma, y traslada la anomalía del tiempo de ejecución, donde se manifiesta como corrupción silenciosa, al tiempo de compilación, donde se manifiesta como un error que te obliga a decidir qué hacer con el NaN. Los traits marcadores como Eq y Ord son la forma que tiene Rust de dejar que un valor afirme que satisface una ley que el compilador no puede demostrar; y el derive, al construir la comparación mecánicamente campo a campo, es la única vía en la que esa afirmación es automáticamente cierta. Comparar, que parecía la operación más inocente del lenguaje, resulta ser un compromiso sobre la estructura algebraica de tus datos.
PartialEq da ==; Eq sella la reflexividad (igualdad total). PartialOrd da < con un partial_cmp que devuelve Option; Ord sella el orden total con un cmp sin Option. f64 es PartialEq y PartialOrd pero nunca Eq ni Ord, porque NaN rompe la reflexividad y la comparabilidad. El derive compara campo a campo en orden de declaración; los sellos son leyes que tú garantizas si implementas a mano.
- Deriva
PartialEq, Eqparastruct Version { mayor: u32, menor: u32 }y compara dos instancias con==. - Añade
PartialOrd, Ordy ordena unVec<Version>con.sort(). Reordena los campos y observa cómo cambia el criterio de orden. - En un
fn main, compruebaf64::NAN != f64::NANy queNAN.partial_cmp(&1.0)daNone. Explica qué ley rompe cada uno. - Intenta usar un
f64como clave de unHashMapy como elemento de unVecque ordenas con.sort(). Lee ambos errores y relaciónalos conEqyOrd. - Ordena un
Vec<f64>sinNaNusando.sort_by(|a, b| a.total_cmp(b))y explica qué responsabilidad asumes al hacerlo.