wandres.dev
TRAITS DEL SISTEMA · Clone, Copy, Debug, Default

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.

⏱ 18 min

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.

🎯 Al terminar esta lección sabrás
  • Comparar con == mediante PartialEq y entender qué añade el sello Eq.
  • Ordenar con <, > mediante PartialOrd y qué garantiza el sello Ord.
  • Explicar por qué f64 es PartialEq y PartialOrd pero nunca Eq ni Ord.
  • Reconocer que derive compara 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.

ℹ️
Si necesitas ordenar flotantes: total_cmp o sort_by

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
}
ℹ️
En los enum, ordena el orden de las variantes

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
}
El sistema de tipos como portador de leyes algebraicas

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.

📝
Lo esencial de igualdad y orden

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.

⚔️ Comparar sin mentir
  1. Deriva PartialEq, Eq para struct Version { mayor: u32, menor: u32 } y compara dos instancias con ==.
  2. Añade PartialOrd, Ord y ordena un Vec<Version> con .sort(). Reordena los campos y observa cómo cambia el criterio de orden.
  3. En un fn main, comprueba f64::NAN != f64::NAN y que NAN.partial_cmp(&1.0) da None. Explica qué ley rompe cada uno.
  4. Intenta usar un f64 como clave de un HashMap y como elemento de un Vec que ordenas con .sort(). Lee ambos errores y relaciónalos con Eq y Ord.
  5. Ordena un Vec<f64> sin NaN usando .sort_by(|a, b| a.total_cmp(b)) y explica qué responsabilidad asumes al hacerlo.