Skip to content
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

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

Closed
wants to merge 14 commits into from
105 changes: 105 additions & 0 deletions text/0000-unsafe-cmp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
- 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.

# 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).

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 `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.
- This is a breaking change.

# Alternatives

- The status quo.
- Have separate traits for trusted behavior and untrusted behavior e.g. `Eq` as
a safe trait that is not trusted, and `EqStrict` that is an `unsafe` trait
that can be trusted by `unsafe` code. The problem is that there is no
obvious reason to implement `Eq` but not implement `EqStrict` (See the
Drawbacks section).

# 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?