-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
RFC: Totally ordered wrappers for f32
and f64
(and possibly others)
#12442
Comments
Is there any generic code built on The #12435 pull request is intended to remove a set of useless traits from the standard library, and make the usable ones into the default. The standard library is simply too complex, and needs to limit the usage of traits to cases where they're helpful for writing generic code. It's silly to have a set of traits for overloading operators not usable by generic code and then another set of traits with real guarantees but no operating overloading. Floating point types do have a total ordering available, so providing a terrible API for the sake of floating point seems silly. I think it's important to keep in mind that if traits only have type signatures but no laws, then the advantage of explicitly declaring type bounds instead of inferring the requirements from the implementation is pretty much gone. The traits aren't really documenting anything about the API if they don't provide either a strict weak order or total order. |
If each trait that provided the guarantees extended the trait that defined the overloaded operator, then I'm not sure that's totally silly. (I mean, it would be a little silly, in that the use of a trait to define the operator is solely an artifact of how Rust does operator-overloading. But not completely silly.) E.g. I'm thinking something like this: #[lang="ord"]
/// This trait is solely to provide access to {<, >, <=, >=} sugared operators,
/// but there are no guarantees about their semantics. It has an intentionally ugly
/// name to discourage use as an accidental trait-bound on type parameters.
pub trait OrdOps {
fn lt(&self, other: &Self) -> bool;
#[inline]
fn le(&self, other: &Self) -> bool { !other.lt(self) }
#[inline]
fn gt(&self, other: &Self) -> bool { other.lt(self) }
#[inline]
fn ge(&self, other: &Self) -> bool { !self.lt(other) }
// FIXME (#12068): Add min/max/clamp default methods
}
/// This trait provides concrete guarantees about ordering.
/// Note that f32/f64 does not impl this trait, but most other numerics do.
trait Ord : OrdOps { } Note that then Having said that, I do not do enough floating-point work myself to push hard to support {<, >, <=, >=} as built-in operators for |
@pnkfelix: IEEE754 defines a total ordering and we are going to implement it, so leaving out an implementation of the total ordering trait for floating point isn't a good option. This means the traits can't have any inheritance relationship if the operators are overloaded with the weak ordering for floating point types. So, there's a trait for generic code and a trait for operator overloading with the overloaded operators not usable in generic code. |
@thestinger my assumption is/was that people working with floating point might prefer to use the operator notation for the non-total-ordering. But I admit this would make it hard/impossible to mix generic-code that uses the |
@thestinger Oh wait, maybe I misunderstood part of your response. You said: "So, there's a trait for generic code and a trait for operator overloading with the overloaded operators not usable in generic code." Does this mean you can have more than one trait providing access to the same overloaded operators? (And presumably choose between them via appropriate use of (Update: rust may get cooler every day, but the above hypothesis is flawed, because you cannot currently have more than one trait with the |
I mean that as long as the floating point numbers use the non-total-ordering for the comparison operators, Rust won't have usable comparison operator overloading in generic code. Even the |
@thestinger So just to be clear, when you said "So, there's a trait for generic code and a trait for operator overloading with the overloaded operators not usable in generic code.", this second trait, not usable in generic code (which presumably would be solely for (I took your use of the word "overloaded operators" to mean "infix sugar", but it seems you could not have meant that.) |
No, I think you're misunderstanding what I'm saying. I am only saying that today, The |
@thestinger Okay. You may not believe this, but I have understood the issues you have describing about I was just musing about a way to try to keep the operator-sugar available for floating-point hackers. I acknowledge that the separation I was proposing has similar problems to that of the existing But I am fine with forcing people who want the hardware |
The previously existing proposal is #12435. The The faster hardware floating point ordering can be provided by the
From a purely performance point of view, LLVM needs you to mark floating point operations with the permitted error and relax the restrictions on floating point transformations to allow generating fast code anyway. |
(nevermind the above, it was written before you posted the latest comment) |
I believe the situation can be summarized as, out of the following:
Choose any two. @thestinger chooses 1. + 3., @pnkfelix is proposing 1. + 2.
|
@glaebhoerl to be honest, when I proposed 1. + 2., I had not thought through the consequences w.r.t. e.g. structs that have I think I'd prefer 1. + 3. over 1. + 2., given our current set of constraints. The only person, I think, who suffers under 1.+3. is the poor floating-point hacker, and while I would love to say "oh haven't they suffered enough?", I think if we choose nice-looking + short names for the methods then we will be okay. |
How much potential is there for people who are not "floating-point hackers" per se, but happen to have stumbled into using Essentially, I think that means, if you're not aware of the intricacies of floating-point math and just (naively) expect the comparison operators to do something reasonable, does the IEEE754 total ordering qualify? |
@glaebhoerl do you mean bitten w.r.t. the unexpected performance hit, or bitten w.r.t. the difference in semantics of the results of the operators? |
For situations that do not involve sorting or containers, the default ieee754 comparison semantics may be preferable, or at least surprising. Having |
@pnkfelix the latter |
I think it has been proposed to put in a lint for people using the infix operators directly on edit: well, maybe it hasn't been previously proposed. |
But if it's a footgun to use them at all, it seems suboptimal to leave it lying around and loaded. (Disclaimer: I'm very much not a floating-point hacker, which is why I'm asking how much of a footgun it is. I'm inferring from the idea that it deserves a lint that it's a significant one.)
Which is just as well! However, they could still do (In theory, if we had something like GHC's Coercible, it would also allow zero-cost conversions between generic types instantiated with |
I just want to remind everyone that there is a reason that the IEEE754 comparison predicates were not defined in terms of the totalOrder predicate. Floating point numbers are not intrinsically totally ordered. One can construct a total ordering over them, but that does not imply that the ordering is appropriate for most uses. I am not aware of any languages that use the total ordering predicate over the standard comparison predicates. Even those that provide an auxiliary total ordering usually do not fully conform to that defined by IEEE754. |
@zkamsler: They are not intrinsically weakly ordered either. There are two valid orderings defined by the standard, and Rust can and should provide both. The comparison operators are defined by the |
@glaebhoerl: Defining the operators with the total ordering predicate is very unlikely to cause any bugs. Nearly all software working with floating point is completely ignoring these kinds of issues anyway, and software written with attention spent on these things has far bigger problems to worry about. It will be far more convenient to be able to sort arrays of floating point numbers and derive |
@glaebhoerl to be honest when I wondered about a lint, I was thinking it would be about the performance pitfall anyway. |
@thestinger I would argue that it is (although weak is perhaps not the right, word having specific meanings). The thing to realize is that an ordering is not the same thing as a comparison. An ordering is free to over-specify. If one is sorting a vector of floats, it does not matter if I do not suggest that the total ordering should not be exposed, but one should not claim that it should be the default basis for the comparison operators. |
@zkamsler: The total ordering guarantee is what generic code requires. If the comparison operators do not use the total ordering traits, then generic code is unable to use the operator overloads. It's not possible to write generic code with an ordering trait when basic guarantees about ordering do not hold.
It's not up to you to say what one should or should not claim. IEEE754 specifies a total ordering, and Rust's generic code needs the total ordering. Traits exist for generic code, so it's going to end up being a requirement for the traits and floating point types might as well implement it so they're usable with generic code and deriving. The traditional ordering will still be available along with functions like I'm open to an alternative solution but no one is offering a viable one. Crippling the comparison traits to the point where they're unusable for the sake of traditional floating point semantics isn't acceptable. Having 2 traits that aren't really ever usable along with 2 more traits that are usable but lack the convenience is a bit ridiculous... If you think the comparison operators are valuable, then push for including user-defined operators, infix call syntax or built-in operators for this. The regular comparison operators belong to |
Rust already uses |
I am cognizant of the needs of generic code, at least to avoid pathological cases involving Even in generic code, the specific totalOrdering specified in the standard may not be the most appropriate. A float-keyed hashtable should probably not put |
It will remain true with the
It needs to put them in different buckets if the hash isn't the same. The new floating point hash doesn't hash these to the same value. The non-totalOrder comparisons are also broken for a hash table due to |
The comparison operators in Rust come from the |
Did you grok the plan in my earlier comment? It would allow non-crippled
Only two of the traits would ever be used for abstraction, the other two ( (Note: I'm not necessarily in favor of this scheme (I'm not really sure myself), I'm arguing for it to help give it proper consideration.)
|
It is invalid in the sense that it represents the same number for (almost) all intents and purposes, and it is difficult to suggest that multiple subnormal representations of the same number should not be treated as equal. as an example:
With the standard comparison predicates, |
It's idiomatic to use it to represent lack of information - this is often how the standard library uses it.
This issue is opposed to landing #12435. I'm fine with having two sets of floating point types, but there's no way we're going to have more than the |
Sure, if you don't want to continue discussing the situation using logical arguments then I'll go find something else to do. Carry on. |
I don't understand why you're saying this. I haven't seen any reasons to keep around 4 comparison traits so I edited #12434 to reflect that there's no consensus on how to fix floating point. There's no reason for it to block changes to the comparison traits because the existing |
@thestinger I'm not willing to shut the discussion down like that, so I'm re-opening the issue. @glaebhoerl and I were not, IMO, suggesting having 4 comparison traits. We were suggesting decoupling the comparison trait from the overloaded infix operators. |
@pnkfelix: There was already a previous issue open about floating point. Decoupling the comparison overloads is suggesting having 4 comparison traits because they are comparison overloads. It's not different than the current situation of having an overly complex API without the ability for generic code to make use of the overloads. |
@thestinger I wholeheartedly agree that
This is true. (Perhaps even more than 4, if we decide to make one trait per operator (as with
This is not true. To be very explicit, let me sketch code:
You're right that this superficially resembles the current situation if we rename Are you philosophically opposed to having separate traits just to define operator overloads? If so, do you have the same objection against Apart from that (which I don't consider a significant issue), the main tradeoffs are around the ergonomics for floating-point types. Version A:
Version B:
|
I closed the pull request and corresponding RFC. The current design of a separate trait for the operator overloads and total ordering can remain in place. I've opened #12517 to cover renaming the traits to reflect the semantics as they are today (as proposed by @glaebhoerl but using I've edited the description in #5585 to reflect that the total ordering needs to be implemented via wrapper types in order to implement the traits. This is a long-standing issue and fixing it isn't a priority, as long as generic code is updated to stop using the operator overloading traits when it needs stronger guarantees. I don't think we need more than these 2 issues. There is clearly no perfect solution to this, and I am not interested in trying to drastically change the design anymore. It just needs to be clearly defined with the existing generic code fixed. |
fix: parsing of `?` opt-out trait bounds fixes rust-lang#12442
…r=y21 [`mut_mut`]: Fix duplicate diags Relates to rust-lang#12379 The `mut_mut` lint produced two diagnostics for each `mut mut` pattern in `ty` inside `block`s because `MutVisitor::visit_ty` was called from `MutMut::check_ty` and `MutMut::check_block` independently. This PR fixes the issue. --- changelog: [`mut_mut`]: Fix duplicate diagnostics
It is well known that
f32
andf64
are not totally orderable by default, and thus does not implementTotalOrd
andTotalEq
. However it is less known that we rarely need the total ordering in reality, and the standard library has only two cases requiring the total ordering (sorting andTreeMap
). #12434 and #12435 are based on this observation, but we can think in the other direction: we can provide totally ordered wrappers for partially ordered types.The suggested wrappers,
TotallyOrderedF32
andTotallyOrderedF64
will be tuple (newtype) structs forf32
andf64
. (Yes, I know this name doesn't look good, it's intentional.) They have the following constraints: (TOF
for any of two types)The actual implementation would be similar to IEEE 754-2008
totalOrder
except thatTOF(-0).cmp(&TOF(+0)) == Equal
, otherwise we would break the constraints as-0 == +0
. Combined withDeref
(#7141, which would have to be implemented eventually) I no longer see the separation ofOrd/Eq
andTotalOrd/Eq
is necessary. Usability-wise, the compiler can suggestTotallyOrderedF{32,64}
on the failure to expand#[deriving(TotalOrd)]
(and others) forf32
andf64
.This RFC only concerns about
f32
andf64
since they are commonly encountered non-totally-ordered types, but if we want to pursue this further, we can alternatively provide aTotalOrder
generic wrapper that massagesOrd
implementors to aforementioned constraints.The text was updated successfully, but these errors were encountered: