|
| 1 | +- Feature Name: `float_next_up_down` |
| 2 | +- Start Date: 2021-09-06 |
| 3 | +- RFC PR: [rust-lang/rfcs#3173](https://github.com/rust-lang/rfcs/pull/3173) |
| 4 | +- Rust Issue: [rust-lang/rust#91399](https://github.com/rust-lang/rust/issues/91399) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +This RFC adds two argumentless methods to `f32`/`f64`, `next_up` and |
| 10 | +`next_down`. These functions are specified in the IEEE 754 standard, and provide |
| 11 | +the capability to enumerate floating point values in order. |
| 12 | + |
| 13 | + |
| 14 | +# Motivation |
| 15 | +[motivation]: #motivation |
| 16 | + |
| 17 | +Currently it is not possible to answer the question 'which floating point value |
| 18 | +comes after `x`' in Rust without intimate knowledge of the IEEE 754 standard. |
| 19 | +Answering this question has multiple uses: |
| 20 | + |
| 21 | + - Simply exploratory or educational purposes. Being able to enumerate values is |
| 22 | + critical for understanding how floating point numbers work, and how they have |
| 23 | + varying precision at different sizes. E.g. one might wonder what sort of |
| 24 | + precision `f32` has at numbers around 10,000. With this feature one could |
| 25 | + simply print `10_000f32.next_up() - 10_000f32` to find out it is |
| 26 | + `0.0009765625`. |
| 27 | + |
| 28 | + - Testing. If you wish to ensure a property holds for all values in a certain |
| 29 | + range, you need to be able to enumerate them. One might also want to check if |
| 30 | + and how your function fails just outside its supported range. |
| 31 | + |
| 32 | + - Exclusive ranges. If you want to ensure a variable lies within an exclusive |
| 33 | + range, these functions can help. E.g. to ensure that `x` lies within [0, 1) |
| 34 | + one can write `x.clamp(0.0, 1.0.next_down())`. |
| 35 | + |
| 36 | + |
| 37 | +# Guide-level explanation |
| 38 | +[guide-level-explanation]: #guide-level-explanation |
| 39 | + |
| 40 | +Because floating point numbers have finite precision sometimes you might want to |
| 41 | +know which floating point number is *right* below or above a number you already |
| 42 | +have. For this you can use the methods `next_down` or `next_up` respectively. |
| 43 | +Using them repeatedly allows you to iterate over all the values within a range. |
| 44 | + |
| 45 | +The method `x.next_up()` defined on both `f32` and `f64` returns the smallest |
| 46 | +number greater than `x`. Similarly, `x.next_down()` returns the greatest number |
| 47 | +less than `x`. |
| 48 | + |
| 49 | +If you wanted to test a function for all `f32` floating point values between 1 |
| 50 | +and 2, you could for example write: |
| 51 | +```rust |
| 52 | +let mut x = 1.0; |
| 53 | +while x <= 2.0 { |
| 54 | + test(x); |
| 55 | + x = x.next_up(); |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +On another occasion might be interested in how much `f32` and `f64` differ in |
| 60 | +their precision for numbers around one million. This is easy to figure out: |
| 61 | +```rust |
| 62 | +dbg!(1_000_000f32.next_up() - 1_000_000.0); |
| 63 | +dbg!(1_000_000f64.next_up() - 1_000_000.0); |
| 64 | +``` |
| 65 | + |
| 66 | +The answer is: |
| 67 | +```rust |
| 68 | +1_000_000f32.next_up() - 1_000_000.0 = 0.0625 |
| 69 | +1_000_000f64.next_up() - 1_000_000.0 = 0.00000000011641532182693481 |
| 70 | +``` |
| 71 | + |
| 72 | +If you want to ensure that a value `s` lies within -1 to 1, excluding the |
| 73 | +endpoints, this is easy to do: |
| 74 | +```rust |
| 75 | +s.clamp((-1.0).next_up(), 1.0.next_down()) |
| 76 | +``` |
| 77 | + |
| 78 | + |
| 79 | +# Reference-level explanation |
| 80 | +[reference-level-explanation]: #reference-level-explanation |
| 81 | + |
| 82 | +The functions `nextUp` and `nextDown` are defined precisely (and identically) |
| 83 | +in the standards IEEE 754-2008 and IEEE 754-2019. This RFC proposes the methods |
| 84 | +`f32::next_up`, `f32::next_down`, `f64::next_up`, and `f64::next_down` with the |
| 85 | +behavior exactly as specified in those standards. |
| 86 | + |
| 87 | +To be precise, let `tiny` be the smallest representable positive value and |
| 88 | +`max` be the largest representable finite positive value of the floating point |
| 89 | +type. Then if `x` is an arbitrary value `x.next_up()` is specified as: |
| 90 | + |
| 91 | + - `x` if `x.is_nan()`, |
| 92 | + - `-max` if `x` is negative infinity, |
| 93 | + - `-0.0` if `x` is `-tiny`, |
| 94 | + - `tiny` if `x` is `0.0` or `-0.0`, |
| 95 | + - positive infinity if `x` is `max` or positive infinity, and |
| 96 | + - the unambiguous and unique minimal finite value `y` such that `x < y` in |
| 97 | + all other cases. |
| 98 | + |
| 99 | +`x.next_down()` is specified as `-(-x).next_up()`. |
| 100 | + |
| 101 | +A reference implementation for `f32` follows, using exclusively integer |
| 102 | +arithmetic. The implementation for `f64` is entirely analogous, with the |
| 103 | +exception that the constants `0x7fff_ffff` and `0x8000_0001` are replaced by |
| 104 | +respectively `0x7fff_ffff_ffff_ffff` and `0x8000_0000_0000_0001`. Using |
| 105 | +exclusively integer arithmetic aids stabilization as a `const fn`, reduces |
| 106 | +transfers between floating point and integer registers or execution units (which |
| 107 | +incur penalties on some processors), and avoids issues with denormal values |
| 108 | +potentially flushing to zero during floating point arithmetic operations |
| 109 | +on some platforms. |
| 110 | + |
| 111 | +```rust |
| 112 | +/// Returns the least number greater than `self`. |
| 113 | +/// |
| 114 | +/// Let `TINY` be the smallest representable positive `f32`. Then, |
| 115 | +/// - if `self.is_nan()`, this returns `self`; |
| 116 | +/// - if `self` is `NEG_INFINITY`, this returns `-MAX`; |
| 117 | +/// - if `self` is `-TINY`, this returns -0.0; |
| 118 | +/// - if `self` is -0.0 or +0.0, this returns `TINY`; |
| 119 | +/// - if `self` is `MAX` or `INFINITY`, this returns `INFINITY`; |
| 120 | +/// - otherwise the unique least value greater than `self` is returned. |
| 121 | +/// |
| 122 | +/// The identity `x.next_up() == -(-x).next_down()` holds for all `x`. When `x` |
| 123 | +/// is finite `x == x.next_up().next_down()` also holds. |
| 124 | +pub const fn next_up(self) -> Self { |
| 125 | + const TINY_BITS: u32 = 0x1; // Smallest positive f32. |
| 126 | + const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff; |
| 127 | + |
| 128 | + let bits = self.to_bits(); |
| 129 | + if self.is_nan() || bits == Self::INFINITY.to_bits() { |
| 130 | + return self; |
| 131 | + } |
| 132 | + |
| 133 | + let abs = bits & CLEAR_SIGN_MASK; |
| 134 | + let next_bits = if abs == 0 { |
| 135 | + TINY_BITS |
| 136 | + } else if bits == abs { |
| 137 | + bits + 1 |
| 138 | + } else { |
| 139 | + bits - 1 |
| 140 | + }; |
| 141 | + Self::from_bits(next_bits) |
| 142 | +} |
| 143 | + |
| 144 | +/// Returns the greatest number less than `self`. |
| 145 | +/// |
| 146 | +/// Let `TINY` be the smallest representable positive `f32`. Then, |
| 147 | +/// - if `self.is_nan()`, this returns `self`; |
| 148 | +/// - if `self` is `INFINITY`, this returns `MAX`; |
| 149 | +/// - if `self` is `TINY`, this returns 0.0; |
| 150 | +/// - if `self` is -0.0 or +0.0, this returns `-TINY`; |
| 151 | +/// - if `self` is `-MAX` or `NEG_INFINITY`, this returns `NEG_INFINITY`; |
| 152 | +/// - otherwise the unique greatest value less than `self` is returned. |
| 153 | +/// |
| 154 | +/// The identity `x.next_down() == -(-x).next_up()` holds for all `x`. When `x` |
| 155 | +/// is finite `x == x.next_down().next_up()` also holds. |
| 156 | +pub const fn next_down(self) -> Self { |
| 157 | + const NEG_TINY_BITS: u32 = 0x8000_0001; // Smallest (in magnitude) negative f32. |
| 158 | + const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff; |
| 159 | + |
| 160 | + let bits = self.to_bits(); |
| 161 | + if self.is_nan() || bits == Self::NEG_INFINITY.to_bits() { |
| 162 | + return self; |
| 163 | + } |
| 164 | + |
| 165 | + let abs = bits & CLEAR_SIGN_MASK; |
| 166 | + let next_bits = if abs == 0 { |
| 167 | + NEG_TINY_BITS |
| 168 | + } else if bits == abs { |
| 169 | + bits - 1 |
| 170 | + } else { |
| 171 | + bits + 1 |
| 172 | + }; |
| 173 | + Self::from_bits(next_bits) |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +# Drawbacks |
| 178 | +[drawbacks]: #drawbacks |
| 179 | + |
| 180 | +Two more functions get added to `f32` and `f64`, which may be considered |
| 181 | +already cluttered by some. |
| 182 | + |
| 183 | +Additionally, there is a minor pitfall regarding signed zero. Repeatedly calling |
| 184 | +`next_up` on a negative number will iterate over all values above it, with the |
| 185 | +exception of +0.0, only -0.0 will be visited. Similarly starting at positive |
| 186 | +number and iterating downwards will only visit +0.0, not -0.0. |
| 187 | + |
| 188 | +However, if we were to define `(-0.0).next_up() == 0.0` we would lose compliance |
| 189 | +with the IEEE 754 standard, and lose the property that `x.next_up() > x` for all |
| 190 | +finite `x`. It would also lead to the pitfall that `(0.0).next_down()` would not |
| 191 | +be the smallest negative number, but -0.0 instead. |
| 192 | + |
| 193 | +Finally, there is a minor risk of confusion regarding precedence with unary |
| 194 | +minus. A user might inadvertently write `-1.0.next_up()` instead of |
| 195 | +`(-1.0).next_up()`, giving a value on the wrong side of -1. However, this |
| 196 | +potential confusion holds for most methods on `f32`/`f64`, and can be avoided |
| 197 | +by the cautious by writing `f32::next_up(-1.0)`. |
| 198 | + |
| 199 | + |
| 200 | +# Rationale and alternatives |
| 201 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 202 | + |
| 203 | +To implement the features described in the motivation the user essentially |
| 204 | +*needs* the `next_up`/`next_down` methods, or the alternative mentioned just |
| 205 | +below. If these are not available the user must either install a third party |
| 206 | +library for what is essentially one elementary function, or implement it |
| 207 | +themselves using `to_bits` and `from_bits`. This has several issues or pitfalls: |
| 208 | + |
| 209 | + 1. The user might not even be aware that a third party library exists, |
| 210 | + searching the standard library in vain. If they find a third party library |
| 211 | + they might not be able to judge if it is of sufficient quality and with the |
| 212 | + exact semantics they expect. |
| 213 | + |
| 214 | + 2. Even if the user is aware of IEEE 754 representation and chooses to |
| 215 | + implement it themselves, they might not get the edge cases correct. It is |
| 216 | + also a wasted duplicate effort. |
| 217 | + |
| 218 | + 3. The user might misunderstand the meaning of `f32::EPSILON`, thinking that |
| 219 | + adding this to a number results in the next floating point number. |
| 220 | + Alternatively they might misunderstand `f32::MIN_POSITIVE` to be the |
| 221 | + smallest positive `f32`, or believe that `x + f32::MIN_POSITIVE` is a |
| 222 | + correct implementation of `x.next_up()`. |
| 223 | + |
| 224 | + 4. The user might give up entirely and simply choose an arbitrary offset, e.g. |
| 225 | + instead of `x.clamp(0, 1.0.next_down())` they end up writing |
| 226 | + `x.clamp(0, 1.0 - 1e-9)`. |
| 227 | + |
| 228 | +The main alternative to these two functions is `nextafter(x, y)` (sometimes |
| 229 | +called `nexttoward`). This function was specified in IEEE 754-1985 to "return |
| 230 | +the next representable neighbor of `x` in the direction toward `y`". If `x == y` |
| 231 | +then `x` is supposed to be returned. Besides error signaling and NaNs, that is |
| 232 | +the complete specification. |
| 233 | + |
| 234 | +We did not choose this function for three reasons: |
| 235 | + |
| 236 | + - The IEEE specification is lacking, and deprecated. Unfortunately IEEE |
| 237 | + 754-1985 does not specify how to handle signed zeros at all, and some |
| 238 | + implementations (such as the one in the ISO C standard) deviate from the IEEE |
| 239 | + 754 standard by defining `nextafter(x, y)` as `y` when `x == y`. |
| 240 | + Specifications IEEE 754-2008 and IEEE 754-2019 do not mention `nextafter` at |
| 241 | + all. |
| 242 | + |
| 243 | + - From an informal study by searching for code using `nextafter` or |
| 244 | + `nexttoward` across a variety of languages we found that essentially every |
| 245 | + use case in the wild consisted of `nextafter(x, c)` where `c` is a constant |
| 246 | + effectively equal to negative or positive infinity. That is, the users would |
| 247 | + have been better suited by `x.next_up()` or `x.next_down()`. |
| 248 | + |
| 249 | + Worse still, we also saw a lot of scenarios where `c` was somewhat |
| 250 | + arbitrarily chosen to be bigger/smaller than `x`, which might cause bugs when |
| 251 | + `x` is carelessly changed without updating `c`. |
| 252 | + |
| 253 | + - The function `next_after` has been deprecated by the libs team in the past |
| 254 | + (see [Prior art](#prior-art)). |
| 255 | + |
| 256 | +The advantage of a potential `x.next_toward(y)` method would be that only a |
| 257 | +single method would need to be added to `f32`/`f64`, however we argue that this |
| 258 | +simply shifts the burden from documentation bloat to code bloat. Other |
| 259 | +advantages are that it might considered more readable by some, and that it is |
| 260 | +more familiar to those used to `nextafter` in other languages. |
| 261 | + |
| 262 | +Finally, if we were to take inspiration from Julia and Ruby these two functions |
| 263 | +could be called `next_float` and `prev_float`, which are arguably more readable, |
| 264 | +albeit slightly more ambiguous as to which direction 'next' is. |
| 265 | + |
| 266 | + |
| 267 | +# Prior art |
| 268 | +[prior-art]: #prior-art |
| 269 | + |
| 270 | +First we must mention that Rust used to have the `next_after` function, which |
| 271 | +got deprecated in https://github.com/rust-lang/rust/issues/27752. We quote |
| 272 | +@alexcrichton: |
| 273 | + |
| 274 | +> We were somewhat ambivalent if I remember correctly on whether to stabilize or |
| 275 | +> deprecate these functions. The functionality is likely needed by someone, but |
| 276 | +> the names are unfortunately sub-par wrt the rest of the module. |
| 277 | +> [...] |
| 278 | +> We realize that the FCP for this issue was pretty short, however, so please |
| 279 | +> comment with any objections you might have! We're very willing to backport an |
| 280 | +> un-deprecate for the few APIs we have this cycle. |
| 281 | +
|
| 282 | +One might consider this a formal un-deprecation request, albeit with a different |
| 283 | +name and slightly different API. |
| 284 | + |
| 285 | +Within the Rust ecosystem the crate `float_next_after` solely provides the |
| 286 | +`x.next_after(y)` method, and has 30,000 all-time downloads at the moment of |
| 287 | +writing. The crate `ieee754` provides the `next` and `prev` methods among a few |
| 288 | +others and sits at 244,000 all-time downloads. |
| 289 | + |
| 290 | +As for other languages supporting this feature, the list of prior art is |
| 291 | +extensive: |
| 292 | + |
| 293 | + - C has `nextafter` and `nexttoward`, essentially identical: |
| 294 | + https://en.cppreference.com/w/c/numeric/math/nextafter |
| 295 | + |
| 296 | + - C++ follows in C's footsteps: |
| 297 | + https://en.cppreference.com/w/cpp/numeric/math/nextafter |
| 298 | + |
| 299 | + - Python has `nextafter`: |
| 300 | + https://docs.python.org/3/library/math.html#math.nextafter |
| 301 | + |
| 302 | + - Java has `nextUp`, `nextDown` and `nextAfter`: |
| 303 | + https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#nextUp-double- |
| 304 | + |
| 305 | + - Swift has `nextUp` and `nextDown`: |
| 306 | + https://developer.apple.com/documentation/swift/double/1847593-nextup |
| 307 | + |
| 308 | + - Go has `Nextafter`: |
| 309 | + https://pkg.go.dev/math#Nextafter |
| 310 | + |
| 311 | + - Julia has `nextfloat` and `prevfloat`: |
| 312 | + https://docs.julialang.org/en/v1/base/numbers/#Base.nextfloat |
| 313 | + |
| 314 | + - Ruby has `next_float` and `prev_float`: |
| 315 | + https://ruby-doc.org/core-3.0.2/Float.html#next_float-method |
| 316 | + |
| 317 | + |
| 318 | +# Unresolved questions |
| 319 | +[unresolved-questions]: #unresolved-questions |
| 320 | + |
| 321 | + - Which is the better pair of names, `next_up` and `next_down` or `next_float` |
| 322 | + and `prev_float`? |
| 323 | + |
| 324 | +# Future possibilities |
| 325 | +[future-possibilities]: #future-possibilities |
| 326 | + |
| 327 | +In the future Rust might consider having an iterator for `f32` / `f64` ranges |
| 328 | +that uses `next_up` or `next_down` internally. |
| 329 | + |
| 330 | +The method `ulp` might also be considered, being a more precise implementation |
| 331 | +of what is approximated as `x.next_up() - x` in this document. Its |
| 332 | +implementation would directly compute the correct [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place) by inspecting the exponent |
| 333 | +field of the IEEE 754 number. |
0 commit comments