Skip to content

Unsafe comparison traits (PartialEq, Eq, PartialOrd, Ord) #956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions text/0000-unsafe-cmp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
- Feature Name: unsafe_cmp
- Start Date: 2015-03-09
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary

Make the four `cmp` traits (`PartialEq`, `Eq`, `PartialOrd`, and `Ord`) become
`unsafe` traits.

`#[derive]` should work as usual, minimizing the amount of `unsafe`s required.

# Motivation

Some algorithms and data structures (such as the `SliceExt::sort()` algorithm
in the standard library) depend on comparisons to be sane in order to be
efficient. In those cases, ill-behaved comparison traits might cause undefined
behavior. However, since the `cmp` traits are currently normal traits (i.e. not
`unsafe`), they cannot be trusted to be well-behaved. As a result, such
optimizations are not possible.

The proposed solution is to make the `PartialEq`, `Eq`, `PartialOrd`, and `Ord`
traits `unsafe`. This allows library to trust the trait implementations to
follow certain rules.

Some might argue that these traits do not invoke `unsafe` behavior. However,
this usage of `unsafe` is intended by design, as described in RFC 19:

> An *unsafe trait* is a trait that is unsafe to implement, because it
> represents some kind of trusted assertion. Note that unsafe traits are
> perfectly safe to *use*. `Send` and `Share` are examples of unsafe traits:
> implementing these traits is effectively an assertion that your type is safe
> for threading.

In the case of comparison traits, the "trusted assertion" is that they behave
sanely, as described in the Detailed design section.

The reason only the `cmp` traits are addressed here is because they have the
highest potential to be relied on by `unsafe` traits. (But see the Unresolved
questions section).

I believe that in practice, only a few `unsafe`s will be required, since most
types will simply `#[derive]` the required traits, in which case the
correctness can be guaranteed.

Additionally, the properties required are made more strict and rigourous in
this RFC.

# Detailed design

`#[deriving]` is not affected by this change. It should work the same as they
always did.

Mark the `PartialEq`, `Eq`, `PartialOrd`, and `Ord` traits as `unsafe` and
require implementations of these traits to satisfy the following properties:

**Note**:
- `=>` stands for "if-then". A property of the form `X => Y` means that "if `X`
type-checks correctly, then `Y` must also do so. If `X` type-checks
correctly and evaluates to `true`, then `Y` must also do so".
- `<=>` stands for "if and only if". A property of the form `X <=> Y` means
that "`X` must type-check correctly if and only if `Y` does so. If they
type-check correctly, they must evaluate to the same boolean value".
- Properties of other forms must evaluate to `true` if they type-check
correctly.

For all comparison traits:
- The comparison methods must not panic.

For `PartialEq`:
- `a.eq(b) <=> b.eq(a)`
- `a.eq(b) && b.eq(c) => a.eq(c)`
- `a.eq(b) <=> !(a.ne(b))`

For `Eq`:
- `a.eq(a)`

For `PartialOrd`:
- `a.partial_cmp(b) == Some(Less) <=> a.lt(b)`
- `a.partial_cmp(b) == Some(Greater) <=> a.gt(b)`
- `a.partial_cmp(b) == Some(Equal) <=> a.eq(b)`
- `a.le(b) <=> a.lt(b) || a.eq(b)`
- `a.ge(b) <=> a.gt(b) || a.eq(b)`
- `a.lt(b) <=> b.gt(a)`
- `a.lt(b) && b.lt(c) => a.lt(c)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably add this form of antisymmetry:
a.lt(b) => !(b.lt(a))
This is in the library documentation already. Of course, the implication must be one-way, because for IEEE NaNs a.lt(b) and b.lt(a) will both be false.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@quantheory It is implied by the other rules.

Proof:
Suppose that a.lt(b) and b.lt(a)
From Rule 6 of PartialOrd (with a and b swapped) and b.lt(a), we get a.gt(b)
From Rule 1 of PartialOrd and a.lt(b), we get a.partial_cmp(b) == Some(Less)
From Rule 2 of PartialOrd and a.gt(b), we get a.partial_cmp(b) == Some(Greater)
Therefore, a.partial_cmp(b) == Some(Less) and a.partial_cmp(b) == Some(Greater), which is absurd.
Thus, the assumption is false, and that one of a.lt(b) and b.lt(a) is false.
Therefore, a.lt(b) => !(b.lt(a))

Correct me if this proof is wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I misread the partial_cmp bits as =>. (Well, there is a tiny problem with your proof, which is that b.lt(a) doesn't have to return at all, but that's not what I meant and not relevant here, probably.)

On a different note, I'm pretty sure that the transitive property is no good here (and as written in the current docs). The reason is that you can define two types in different crates that can compare to the same type in some third crate, but not to each other. Then if you use all three together there's suddenly a problem. It's a modularity hazard.

I think that transitivity should only be required if all of the comparisons are defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a.lt(b) && b.lt(c) => a.lt(c)

It's impossible to guarantee this unless adding an implementation of PartialOrd is a breaking change.

Consider two crates A and B. B defines a type Y. A defines type X and implements the <= operator between X and Y. Now B defines a new type Z and implements the <= operator for Y and Z. Then X <= Y and Y <= Z are defined but X <= Z is not defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see that @quantheory already mentioned this.


For `Ord`:
- `Some(a.cmp(b)) == a.partial_cmp(b)`

# Drawbacks

- Some types might want to implement these traits such that they do not satisfy
these properties. However, I consider this to be abuse of traits.
- Some people might just use `unsafe` without knowing the potential bad
consequences.
- Might cause too many `unsafe`s in otherwise safe code.
- Incorrect implementations might accidentally cause unsafe behavior.
- This is a breaking change.

# Alternatives

- The status quo.
- Have an `unsafe fn is_correct() -> bool` or a similar method that defaults to
returning `false`, but have `#derive` return `true` only if all of the
type's fields' `is_correct()` return `true`. It is still unclear about
where (i.e. in which trait) should this method be placed. It might also be
better to use associated constants for this purpose.
- Have separate `RelaxedEq` and `RelaxedOrd` traits with untrusted behavior,
and make the original four traits `unsafe` and have trusted behavior. Make
the operator overloading use the `Relaxed` versions. Have separate
functions that depend on and do not depend on sane comparisons. One problem
about this is handling `#[derive]`s properly.
- Similar to previous alternative, but have `EqStrict` and other traits that
are `unsafe` instead.
- Make `PartialEq` and `PartialOrd` normal traits without any guarantees, but
make `Eq` and `Ord` `unsafe` traits with guarantees.

# Unresolved questions

- Are the properties required here complete?
- Is this worth the number of extra `unsafe`s?
- What about the `Iterator`, `ExactSizeIterator`, `DoubleEndedIterator`, and
`RandomAccessIterator` traits?
- Does this apply to other traits?
- Can the transitivity properties be enforced cross-crate?
- Do we need the type parameters in `PartialEq` and `PartialOrd`?
- What about functions that take custom comparison functions like `sort_by()`?