Something has been bothering me.
Consider the following wrapper type, which provides a custom Ord for the purpose of e.g. usage in BTreeSet
:
use std::cmp::Ordering;
use std::collections::BTreeSet;
// A wrapper around (i32,i32) that compares by sum.
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
struct Wrapper(i32, i32);
impl Wrapper {
fn key(self) -> i32 { self.0 + self.1 }
}
impl PartialOrd for Wrapper {
fn partial_cmp(&self, other: &Wrapper) -> Option<Ordering> {
self.key().partial_cmp(&other.key())
}
}
impl Ord for Wrapper {
fn cmp(&self, other: &Wrapper) -> Ordering {
self.key().cmp(&other.key())
}
}
fn main() {
let vec = vec![Wrapper(2, 3), Wrapper(5, 4), Wrapper(1, 4)];
let set: BTreeSet<_> = vec.into_iter().collect();
assert_eq!(
set.into_iter().collect::<Vec<_>>(),
vec![Wrapper(2,3), Wrapper(5,4)]
);
}
Looks innocent, right? Perhaps others themselves have even written such a type at least once.
But there is, in fact, a subtle problem with this type:
a == b
is not equivalent to a.cmp(&b) == Ordering::Equal
.
This violates a contract which is embedded in the very fact that PartialOrd: PartialEq
and Ord: Eq
.
To be honest, this relationship isn’t one that I tend to think about. In my mind, equivalence and order are orthogonal concepts, and it wouldn’t surprise me if at some point in the past I had written a type just like Wrapper
, unaware of the contract it violates.
What do others think? How prevalent might types like Wrapper
be in existing rust code, and are they ticking time bombs or no big deal? Could/Should something be done to discourage their creation?