Description
Proposal
Problem statement
A lot of the existing code for floating-point values will special-case infinite and NaN values, and it's desirable to be able to specify at the type level that these values cannot occur. Additionally, the ability to remove NaN values from floats would allow the ability to implement Ord
for these types, enabling their use in many APIs without relying on total_cmp
.
Motivating examples or use cases
- Inclusion in types which require
Ord
invariants, such as the keys of aBTreeMap
or the values in aBinaryHeap
. - Storing values which are interpolated in various contexts, such as layout engines or stored statistics, since interpolating these values can cause undesired behaviour.
- Floating-point values stored in enums which leverage niche optimisations, reducing memory usage by storing sentinel NaN values.
- High-performance floating-point code which wishes to avoid special-casing NaN and infinite values.
Notes on optimisation as motiviation
One potential benefit of these types is the ability to add additional optimisations to them. For example, operations like sin
, cos
, and hypot
could all be modified to return values that are explicitly non-NaN to avoid additional checks for NaN in code.
However, note that these cases can be relatively niche and it's not clear how many of these would be actually useful. Additionally, operations that are more complicated like linear interpolation would have to be included in the standard library, and we've already mostly decided that these operations are not a good fit for the standard library due to their complexity.
Additionally, many operations would only be easily optimised if the floats were further constrained to be explicitly positive, non-zero, non-one, or bounded in some other arbitrary way. This definitely reduces the usefulness of this avenue of optimisation, and it feels best to only analyse these types as being useful specifically for their invariants, rather than for their ability to optimise various mathematical operations, at least within the standard library. Obviously, downstream crates can leverage these types for their own optimisations, assuming that they're okay with using unsafe code.
Solution sketch
Four types would be added: NonNanF32
and NonNanF64
for f32
and f64
types which exclude NaN values, and FiniteF32
and FiniteF64
for types which exclude both NaN values and the two infinities.
The minimum API for these types should mimic that of the NonZero*
types, namely (using NonNanF32
as an example):
pub const unsafe fn new_unchecked(x: f32) -> NonNanF32;
pub const fn new(x: f32) -> Option<NonNanF32>;
pub const fn get(self) -> f32;
Additionally, these would implement all of the common traits from their underlying floating-point types (PartialEq
, PartialOrd
, Copy
, Debug
, etc.) but also implement Eq
, Ord
, and Hash
due to the lack of NaN values. Unlike the NonZero*
types, Default
would be fine, since zero would still be included.
These types would additionally include niche areas for the forbidden values, which is possible due to the fact that non-finite values are represented by two continuous ranges (one for each sign) when treated as integer bits. The infinities also exist at the edges of these ranges adjacent to the finite values and can easily be represented.
Alternatives
The primary alternative would be to simply avoid adding these types, as there are many crates that already exist to provide these types. The largest benefits of a standard library implementation would be to standardise these types, provide niche optimisations (at least until a stable avenue exists), and to potentially optimise various mathematical operations on these types with compiler help, although that last bit is disputed in the motivation section.
Links and related work
- RFCs issue discussion: Idea: NonZero-like float wrappers rfcs#2458
- Internals thread discussion: https://internals.rust-lang.org/t/pre-pre-rfc-range-restricting-wrappers-for-floating-point-types/6701
- Popular external crate solution: https://docs.rs/decorum/latest/decorum/
What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
- We think this problem seems worth solving, and the standard library might be the right place to solve it.
- We think that this probably doesn't belong in the standard library.
Second, if there's a concrete solution:
- We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
- We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.