Is `None` returned from `partial_cmp` supposed to only signify `NaN`?

I have an enum:

enum Foo {
    A(u32),
    B(u32),
}

And I wonder if it makes sense to implement PartialOrd. Comparing A to B is impossible without external information but they can in principle be compared. Thus the derived implementation is confusing at best. To not bother you with domain knowledge, same problem would occur if A and B was Eur and Usd - you can compare them if you know the exchange rate.

So it seems the implementation could return None for distinct variants and Some(_) for same variants comparing inner values. Similarly for PartialEq.

What I'm not sure is if this implementation is logically sound. IOW, I know partial_cmp() -> Option and all those traits were designed to reasonably handle floats/NaNs. Can it be reasonably used in my case too or should it only be used for NaN-like situations?

1 Like

Yes. PartialOrd in Rust has the exact same meaning as in mathematics when you say "partial order on a set", aka, an ordering exists only between some elements.

6 Likes

Clearly explained, thanks!

It’ a bit more general than that mathematical notion; admitted it’s still (mis-)documented as being about a “partial order”. I’ve semi-recently participated in a way too long discussion about those points; I don’t fully remember the outcome, I think the main differences were that

  • it’s not clear what notion of “equality” to work with in the context of programming, and depending on how you’re interpreting things, you could be talking about e.g. interpreting Ord as a weak ordering, and PartialOrd together with Eq would be a preorder
  • PartialOrd with only PartialEq and not Eq allows a value to be unequal to itself, which is completely different from any typical “ordering” relations in mathematics

On the other hand PartialOrd can be used for “partial orders” too, just it also allows even more other thing beyond “partial orders”. For your use case, this means that @RedDocMD's answer is still correct, you can use it. You can even implement Eq as well if you have a “partial order”, and you can use “partial order”-axioms to verify that you have, in particular, a valid PartialOrd implementation, too.

Also note that equality checking via PartialEq would still always return true or false; there’s no way to make that, too return None, even though one could argue that comparing an A and a B for equality should give an answer “unknown!” rather than “no!”, too. I guess it isn’t too bad though, just use partial_cmp.

9 Likes

That caveat also applies to PartialOrd::{lt, le, gt, ge}. These all return false when partial_cmp returns None. So for example, you should be careful that !lt does not imply ge.

2 Likes

I wish partial_cmp returning None was far more common, TBH. I've never seen a solid justification for why Ok is always less than Err, for example, as opposed to the other way around. And needing to have the full n² set of arms in comparisons in order to be total (as opposed to n+1 for partial) has always felt unfortunate to me.

So :+1: from me on just having your partial_cmp be

match (x, y) {
    (A(x), A(y)) => x.partial_ord(y),
    (B(x), B(y)) => x.partial_ord(y),
    _ => None,
}
2 Likes

The difference is that while PartialOrd::{lt, le, gt, ge} may all return false at the same time, the same is not true for PartialOrd::{eq, ne}, unless you’re violating the conditions explained in its documentation:

Implementations must ensure that eq and ne are consistent with each other:

  • a != b if and only if !(a == b) (ensured by the default implementation).

The only thing differentiating PartialEq from Eq is that the latter requires x == x to be always true while for PartialEq it can (and is in case of NaN) be false, and that PartialEq can work between different types, while for Eq the left-hand-side and right-hand-side argument needs to be of the same type.

The problem is that Ord (together with PartialOrd) serves multiple purposes

  • to provide a semantic notion of ordering that makes sense for the given type
    • under this aspect, ordering Ok vs. Err doesn’t make too much sense
  • to offer an implementation of some arbitrary fixed ordering for search-trees such as BTreeMap/BTreeSet
    • for this, having an Ord implementation for Result is super useful, and the arbitrary choice of how to order Ok vs Err doesn’t matter much, it just has to be decided either way and then it becomes mostly an irrelevant implementation detail anyway (ignoring BTree… operations like range or split_off where the ordering does matter)
  • to offer an overloading of </<=/>/>= operators
    • for this, it’s kind-of redundant/overkill/limiting to have any constraints on their implementations listed at all on the respective traits; after all, operators like + allow any arbitrary operation, too, and don’t document any laws those should uphold, such as (a + b) + c) == a + (b + c) and the like; even the fact that they always need to return bool seems somewhat limiting
3 Likes

I agree with what you wrote, but

this is the kind of thing where I get frustrated with it.

And there's lots of places where it ends up causing weird asymmetry, like .fold(None, cmp::min) and .fold(None, cmp::max) do completely different things. For all that NAN can be annoying, at least .fold(NAN, f32::min) and .fold(NAN, f32::max) do analogous things.

Without having read the context of this thread, I would like to quote my point on whether:

My p.o.v. was this one:

And: