Closed steffahn closed 9 months ago
Those requirements are for values that are actually comparable.
@sfackler Let me repeat what I already stated:
focus on the final paragraph in the quote above
and that final paragraph is
Note that these requirements mean that the trait itself must be implemented symmetrically and transitively: if
T: PartialOrd<U>
andU: PartialOrd<V>
thenU: PartialOrd<T>
andT: PartialOrd<V>
.
Ah right - we already changed that language for PartialEq: #81198. We should make the same change here IMO.
The solution that PartialEq
takes isn’t perfect either. The following example is a bit artificial because I couldn’t find or come up with a real/realistic one (yet). According to those docs, if I have one crate a
with
// crate `a`
pub struct A(pub u8);
and a crate b
depending on a
// crate `b`, depends on `a`
#[derive(PartialEq)]
pub struct B;
then – AFAICT – this kind of PartialEq
implementation would be allowed
// still in crate `b`
use a::A;
impl PartialEq<A> for B {
pub fn eq(&self, _other: &A) -> bool {
true
}
}
impl PartialEq<B> for A {
pub fn eq(&self, _other: &B) -> bool {
true
}
}
However, now the crate a
could make a minor version change adding a PartialEq
implementation itself:
// in crate `a`, minor version update adding:
impl PartialEq for A {
pub fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
// or equivalently, adding `#[derive(PartialEq)]` to `A`
AFAIK, minor version updates are allowed to add trait impls such as the PartialEq for A
one above. But now we have
A(0) == B && B == A(1)
but
A(0) != A(1)
In what context are those PartialEq
implementations actually reasonable?
This PartialEq
documentation also doesn’t disallow something like
struct A(u8);
struct B(u8);
struct C(u8);
struct D(u8);
impl PartialEq<B> for A {
fn eq(&self, other: &B) -> bool {
self.0 == other.0
}
}
impl PartialEq<A> for B {
fn eq(&self, other: &A) -> bool {
self.0 == other.0
}
}
impl PartialEq<C> for B {
fn eq(&self, other: &C) -> bool {
self.0 == other.0
}
}
impl PartialEq<B> for C {
fn eq(&self, other: &B) -> bool {
self.0 == other.0
}
}
impl PartialEq<D> for C {
fn eq(&self, other: &D) -> bool {
self.0 == other.0
}
}
impl PartialEq<C> for D {
fn eq(&self, other: &C) -> bool {
self.0 == other.0
}
}
impl PartialEq<A> for D {
fn eq(&self, other: &A) -> bool {
true
}
}
impl PartialEq<D> for A {
fn eq(&self, other: &D) -> bool {
true
}
}
since it doesn’t disallow a situation where
b == c && c == d && d == a
// but
b != a
// using
let (a, b, c, d) = (A(1), B(0), C(0), D(0));
Similarly, this wouldn’t be disallowed either
struct A(u8);
struct B(u8);
struct C(u8);
impl PartialEq<B> for A {
fn eq(&self, other: &B) -> bool {
self.0 == other.0
}
}
impl PartialEq<C> for B {
fn eq(&self, other: &C) -> bool {
self.0 == other.0
}
}
impl PartialEq<A> for C {
fn eq(&self, other: &A) -> bool {
true
}
}
where we’ll have
b == c && c == a
// but
a != b
// using
let (a, b, c) = (A(1), B(0), C(0));
These kinds of examples can also be used for new examples like in my previous comment demonstrating that semver makes it unclear which crate needs to ensure which kinds of properties: imagine C
being in a different crate (together with the two impl
s involving C
). Then a future version of the crate containing A
and B
could add the missing symmetric impl PartialEq<A> for B
implementation and this change introduces a case of b == c && c == a && b != a
.
The documentation states useful properties that code working with partial equality can assume. I don't think it is necessary or even particularly useful to guard against every hypothetically possible malicious or bizarre combination of impls.
In any case, I agree that the conditions as in PartialEq
are better than before and better than the ones in PartialOrd
.
Ah right - we already changed that language for PartialEq: #81198. We should make the same change here IMO.
That sounds reasonable.
Similarly, this wouldn’t be disallowed either
So the point is that if b == a
would type-check then it would be required to hold (and then a != b
would be forbidden), but there is a "gap" in the chain and thus this fails to be disallowed?
These kinds of examples can also be used for new examples like in my previous comment demonstrating that semver makes it unclear which crate needs to ensure which kinds of properties: imagine C being in a different crate (together with the two impls involving C). Then a future version of the crate containing A and B could add the missing symmetric impl PartialEq for B implementation and this change introduces a case of b == c && c == a && b != a.
This still requires C
to have a clearly weird notion of equality though, doesn't it? Namely it depends on the crate defining A
and B
and provides a way to (transitively) compare A
s and B
s in a way that disagrees with directly comparing them. So IMO it is quite clear where the bug is here, even if we don't go through the effort of making the docs completely water-tight in this regard. (We'd have to also state some symmetric cases of transitivity to achieve that.)
So the point is that if b == a would type-check then it would be required to hold (and then a != b would be forbidden), but there is a "gap" in the chain and thus this fails to be disallowed?
Yes
Namely it depends on the crate defining
A
andB
and provides a way to (transitively) compareA
s andB
s in a way that disagrees with directly comparing them.
That’s true…
it is quite clear where the bug is here
I agree, intuitively it’s clear
even if we don't go through the effort of making the docs completely water-tight in this regard
My problem is perhaps more the problem that I’m not even sure if there exists a way to make it watertight without contradictions at all. And even if there is a way there might be multiple ways to make this mathematically accurate and watertight. And then one crate creator could intuitively have one understanding of the rules while another one could have a different, incompatible intuition (either intuition might be justified by one of the multiple ways to make the situation formally rigid), with the incompatibility of the approaches resulting in violation of even the minimal set of rules we’d like to have.
TL;DR: What I already said, “semver makes it unclear which crate needs to ensure which kinds of properties”. We have no story of who’s responsibility for what in order to enforce the rules we want to have. The problem seems highly non-trivial because you’d need to consider semver as well as orphan rules. Maybe there’d be a need to establish additional orphan-rule-like restrictions, e.g. the example of a type C
being introduced as a means to – indirectly – compare two previously uncomparable types A
and B
. So this might be disallowed by some “orphan rule”-style convention of not introducing any way to compare two external types A
and B
even indirectly. Introducing direct comparison in this case is already prevented by ordinary orphan rules preventing impl PartialOrd<B> for A
, but the indirect case would IMO be best documented in the docs of the respective traits like PartialOrd
.
Yeah I agree it's not entirely trivial. Basically, instead of reasoning about the impls that currently exist, you have to reason about the impls that might exist in the future -- "For all legal supersets of the current set of implemented traits, if A: PartialEq<B>
and B: PartialEq<C>
and A: PartialEq<C>
, then ..."
This can be made precise with a "multiple worlds" kinds of model -- coherence already talks about "compatible words" and I recall blog posts along those lines but can't find them right now. The statement in the PartialEq
doc must be interpreted not in the current worlds, but in any compatible (future) world.
This was briefly discussed in the library api meeting last week. Our conclusion was that since we already accepted #81198 for PartialEq
, we should accept the same change for PartialOrd
.
The problem with how PartialEq
conditions have been changed is that now it contradicts the statement that PartialEq
implements partial equivalence relations.
See this IRLO thread.
Example code from the thread:
enum A {
A1,
A2,
}
enum B {
B1,
B2,
}
impl PartialEq<B> for A {
fn eq(&self, other: &B) -> bool {
match (self, other) {
(A::A1, B::B1) => true,
(A::A1, B::B2) => true,
(A::A2, B::B1) => true,
(A::A2, B::B2) => false,
}
}
}
This satisfies the PartialEq
conditions, but is inconsistent with it being a partial equivalence relation.
Therefore any generic code bound by PartialEq
cannot assume that the relation between different types forms a partial equivalence relation, rendering these conditions more or less useless.
I think the solution is to only require these conditions for T: PartialEq<T>
and T: PartialOrd<T>
and not between different types.
An even more drastic potential solution would be to only have these symmetry and transitivity requirements for Eq
and Ord
, and not for PartialEq
and PartialOrd
. I haven't seen a practical use case requiring a partial equivalence relation or a partial order but not Eq
or Ord
.
This satisfies the PartialEq conditions, but is inconsistent with it being a partial equivalence relation.
The notion of a PER is defined on binary relations over some set X. I am not aware of it even being a well-defined question to ask whether a relation on X x Y
(like your example) is a PER. Where are you taking that notion from? Your Wikipedia link does not provide a definition that could be applied here.
We should probably clarify the docs to say that "if T == U
, then these rules imply that the relation is a PER".
We should probably clarify the docs to say that "if T == U, then these rules imply that the relation is a PER".
Agreed!
But what is the value of having such rules for T != U != W
if they don't even imply anything like PER? They still seem tricky to satisfy when different people implement different parts of this in different crates, even the weaker version.
The rules seem totally useless if you can't even deduce basic equivalence properties between objects, such as "when a = x, a = y, b = x, then b = y".
The notion of a PER is defined on binary relations over some set X.
Agreed. That's one reason why I suggested weakening the conditions to only apply to T == U
.
I am not aware of it even being a well-defined question to ask whether a relation on X x Y (like your example) is a PER. Where are you taking that notion from?
I am taking the notion that it should be a PER directly from the first line of documentation of PartialEq
, and from the name of the trait itself.
You could in theory have PER between different types: it's just same thing as Wikipedia says, as a relation on (T ∪ U) x (T ∪ U)
. So my point was: my example violates PER for all possible ways that A: PartialEq<A>, B: PartialEq<B>, B: PartialEq<A>
could be implemented, and violates various corollaries of these elements being in some partial equivalence relation.
In another world, one would more reasonably define three traits like this:
T: EqualityOperator<U>
(no requirements, except perhaps that x != y
is the same as !(x == y)
)PartialEq : EqualityOperator<Self>
(partial equivalence relations)Eq : PartialEq
(equivalence relations)Frankly, I don't really see the practical value of even having a trait for "partial equivalence relations". Is there some code that specifically wants partial equivalence relations? I doubt it. Hence my other possible proposal: given that PartialEq
is defined on two types, treat it as if it was EqualityOperator
and not have any conditions on it.
Having a trait require certain things for T == U
only might be a bit strange for generic implementation that doesn't know whether T == U
. But perhaps it's fine.
I suspect the only reason the trait was conceived with that name in the first place is that somebody noticed that IEEE-754 floating point equality happens to satisfy this strange concept of partial equivalence relations, not because anything required such a trait specifically.
I am taking the notion that it should be a PER directly from the first line of documentation of PartialEq, and from the name of the trait itself.
Your previous post presupposes that there is some definition of "PER on (A, B)
", which is not the case.
The stdlib docs also presuppose this, but your example doesn't demonstrate a problem with the stdlib docs, it just repeats the same mistake that the stdlib docs made by using a term outside of the domain it is defined on.
You could imagine defining PER on different types as the same thing as the mathematical PER on the union of T ∪ U
That doesn't seem like a sensible definition to me. Symmetry is impossible to satisfy for elements in the symmetric difference of T
and U
, meaning those elements would be forced to be unrelated. (Rust enum types are nominal, so mathematically A::A1
and B::B1
are distinct values and the only heterogeneous PERs would be the empty relations.)
I suspect the only reason the trait was conceived with that name in the first place is that somebody noticed that IEEE-754 floating point equality happens to satisfy this strange concept of partial equivalence relations, not because anything required such a trait specifically.
When precedence for such a concept exists in mathematics, I think it would be foolish to deviate from that without a good justification. It's also not at all a strange concept; partial orders are pretty common and partial equivalence relations are the natural name for a partial order that is also symmetric.
But anyway, we seem to agree that the docs should emphasize PERs less. It still seems like a useful fact to mention for the common case where T == U
.
But note that if you made this choice, your example would be a PER, since
T
andU
are disjoint. (Rust enum types are nominal, so mathematicallyA::A1
andB::B1
are distinct values.)
My point is, my implementation of PartialEq
on A x B
is not a subset of any partial equivalence relation on (A∪B) x (A∪B)
, which is what one might expect. The practical issue with that is that there are certain properties you'd expect from a partial equivalence relation, even if you don't know how some of the comparisons work, and those properties are violated here.
Symmetry is impossible to satisfy for elements in the symmetric difference of T and U
What? That makes no sense. First, by T ∪ U
I meant the union of T
and U
(I guess the symmetric difference is the same thing as the union, since these are disjoint).
But secondly: it's certainly possible to satisfy symmetry on the union of two types.
For instance, the implementation of PartialEq
for String
and &str
is symmetric and transitive for all combinations of values and types, and is indeed a partial equivalence relation on the union String ∪ &str
.
When precedence for such a concept exists in mathematics, I think it would be foolish to deviate from that without a good justification.
I am not proposing deviating from anything... Just not using it. C++ doesn't use such a concept and is doing fine.
But anyway, if you want to keep the T == U
case I think that is reasonable too.
The symmetric difference is T\U ∪ U\T
. It's basically XOR on sets.
I am not proposing deviating from anything... Just not using it.
If the rules are carefully picked to ensure that they form a PER for the homogeneous case, it would IMO be silly not to mention that.
The symmetric difference is
T\U ∪ U\T
.
Yes, and when T
and U
are disjoint, that is the same thing as T ∪ U
, since T\U == T
and U\T == U
.
If the rules are carefully picked to ensure that they form a PER for the homogeneous case, it would IMO be silly not to mention that.
Well yes, assuming the rules are kept then it should be mentioned that it's PER. I was talking in the context of a "plan B" I had proposed, where we remove those rules altogether:
An even more drastic potential solution would be to only have these symmetry and transitivity requirements for Eq and Ord, and not for PartialEq and PartialOrd.
My statement is that no element of T\U
can be on the LHS of a such-defined PER that is defined as a Rust instance of PartialEq<U> for T
. Doing so would require having the same element also to the right of some pair in the relation, which is ill-typed in Rust.
But anyway, your ad-hoc extension of PERs to heterogeneous types is not really relevant for us here.
But anyway, your ad-hoc extension of PERs to heterogeneous types
It's not "my ad-hoc extension" -- before #81198 it was actually required by the docs.
No it was not. The docs never said to consider a relation on (T ∪ U) x (T ∪ U)
.
No it was not. The docs never said to consider a relation on
(T ∪ U) x (T ∪ U)
.
I feel like you're just trying to trap me on some word games. They didn't say so in those words, but what they did say mathematically implied it.
The docs implied that if ==
on T x U is defined, then it must also be defined on U x T, and also (by transitivity) on T x T and on U x U.
In other words, the rules implied that it must be defined on (T ∪ U) x (T ∪ U)
, because mathematically (T ∪ U) x (T ∪ U) = (T x U) ∪ (U x T) ∪ (T x T) ∪ (U x U)
.
The requirements also stated that ==
must be symmetric and transitive for all combinations of these types. Which is the definition of the concept "partial equivalence relation" on (T ∪ U)
.
The docs also did say explicitly that this constitutes a "partial equivalence relation". And they were correct.
After the change, it's no longer correct except for T = U.
They didn't say so in those words, but what they did say mathematically implied it.
No they did not. They used (and use) the term PER in an ill-defined way. That doesn't imply anything, except that the docs need to be fixed.
https://github.com/rust-lang/rust/pull/115386 intends to fix this for PartialEq. If that lands, we can then do the same for PartialOrd.
Std implementations of PartialOrd are violating the conditions regarding transitivity and symmetry.
From the standard library docs:
(emphasis mine)
focus on the final paragraph in the quote above and look at the following example
(in the playground)
So either the library documentation is off or the implementations are flawed.
And the transitivity requirements are
impossiblehard to ensure in a multi-crate ecosystem anyways.Note that, technically, it’s impossible to enforce transitive existence of impls for the trait unless using operands of fully “external” types is completely prohibited:
If I’m writing a crate
foo
providing a typestruct Foo(…);
and animpl PartialOrd<i32> for Foo
as well as animpl PartialOrd<Foo> for i32
, it seems like I’m following the rules set byPartialOrd
’s documentation.If I’m writing a crate
bar
providing a typestruct Bar(…);
and animpl PartialOrd<i32> for Bar
as well as animpl PartialOrd<Bar> for i32
, it seems like I’m following the rules set byPartialOrd
’s documentation.Now, if I’m writing a third crate that imports both
foo
andbar
, then I’ll haveimpl PartialOrd<Foo> for i32
as well asimpl PartialOrd<i32> for Bar
, but obviouslyimpl PartialOrd<Foo> for Bar
is missing.In this example, the crates
foo
andbar
each provided animpl
where one of the operands,i32
, was a type that’s fully external to the crate itself (in the sense that neither the type nor any of its generic arguments are part offoo
, or part of a different crate that has the same owners asfoo
[i32
has no generic arguments to begin with]).@rustbot label C-bug, T-libs