Why are float equality comparsions allowed?

While reading Rust Brain Teasers by Herbert Wolverson it struck me as odd that Rust allows for implicit equivalence comparisons between floats.

It's common to see the allowance of evil float comparisons in many other languages (x == y), and many would expect Rust to be no different, but it seems... out of character. Rust has this way of paving over potholes or painting signs to reduce the likelihood of ignorance. For instance, panicking on overflow, requiring the use of Wrapping, or native UTF-8 support within chars.

Yet, Rust implements ParitalEq for f32 allowing for innocent-looking code (1.0 + 1.0 == 2.0) to fail at runtime with no explanation.

Why does f32 implement ParitalEq? Is there a technical reason? Does the problematic comparison seem obvious enough? Would there be dire consequences if the implementation were removed and replaced with a different solution? What are some possible solutions? Are existing crates good enough?

Slightly related threads to chew on:

  • _https://users.rust-lang.org/t/implement-trait-for-is-close-for-types-that-have-an-epsilon/61873/14
  • _https://github.com/rust-lang/rust/issues/41620
  • _https://github.com/rust-lang/rust/pull/84045

(sorry for the malformed urls, new users only allowed 2 links)

2 Likes

Equality for floats is well-defined, and Rust's implementation reflects that. 1.0 + 1.0 == 2.0 is also valid as far as computers go - it trips up your maths intuition, but then again so do all functions with side-effects. Banning this in the standard library will make little sense - apart from the other glaring problem that PartialOrd requires PartialEq (which makes mathematical sense, you can't partially order objects if you can't "identify" them).
On the other hand, floats don't implement Ord but implement PartialOrd because of NaN.
Also, there is a Clippy lint for this, so people won't be tripped up.

7 Likes

That's why it has Ord, which floats are not.

But f32 is an IEEE 754 single-precision floating point. That defines equality, and thus Rust offers it.

There's really no good alternative. It's impossible to represent arbitrary real numbers in computers, and especially if you only have 32 bits.

Saying you can't use == doesn't really fix anything, because making people use x <= y && x >= y isn't better. Not to mention that the other comparisons are equally fallible -- one wouldn't expect 0.1 + 0.2 > 0.3 to be true, but it is -- so are you planning on removing all the comparisons? That doesn't seem right either.

There's no canned "just do this and you'll be fine" solution. IEEE 754 is about as good as it gets, in finite space, and has the huge advantage of lots of literature for people who do want to make sure they're doing it correctly.

(And, before someone inevitably brings them up, no, posits don't solve anything here.)

12 Likes

There are perfectly valid use-cases for float equality. For example, if you compare a float with a previous version of the float, then you can check if the float has been changed since then.

8 Likes

That doesn't fail. Both 1.0 and 2.0 are exactly representable in f32 and f64 alike, and it's not like people compare two literal numbers in their programs anyway. Exact comparison is needed when you want to e.g. check for division by zero, for one, but there are many more examples.

It is also the case that Clippy lints against exact comparisons, so there is no reason why they should be flat out disallowed.

8 Likes

I think equality is no special case. With floating-point numbers, some things work surprisingly well (such as 1.0 + 1.0 == 2.0, which people often believe isn't guaranteed with IEEE 754, but in fact it is, as @H2CO3 pointed out), and other things can go wrong. The things that can go wrong aren't limited to equality comparisons. Consider the following example:

fn main() {
    let mut x = -1.0;
    for _ in 0..100 {
        x /= 2.0;
    }
    assert!(1.0 / x < -2.0); // this works fine
    assert!(!(1.0 / x >= -2.0)); // as well as this
    assert!(1.0 + 2.0 == 3.0); // this works fine also
    assert!(f64::sqrt(0.3 - 0.2 - 0.1) > -1.0); // this fails
}

(Playground)

Errors:

… 'assertion failed: f64::sqrt(0.3 - 0.2 - 0.1) > -1.0' …
  • 1.0 / x < -2.0: this works fine (even though if x == 0.0),
  • !(1.0 / x >= -2.0) this works fine also (to demonstrate that this isn't a weird NaN case),
  • 1.0 + 2.0 == 3.0 works without problems (note that for example Lua until version 5.2 didn't provide integers at all and would do any operation with floats!),
  • f64::sqrt(0.3 - 0.2 - 0.1) > -1.0 this fails, even though it doesn't even contain an equality comparison.

My point is: working with floating-point numbers is always tricky. If we forbid ==, then we should also forbid <, <=, >, >=, and maybe also x/y? This doesn't seem reasonable to me; however, I wonder if we did (forbiddng these operators for floating-point numbers), would this have made the PartialOrd and PartialEq traits superfluous? So maybe it would also have had advantages.


Edit: We need to divide by 2.0 more often to truly get x == 0.0:

-    for _ in 0..100 {
+    for _ in 0..2000 {
         x /= 2.0;
     }
+    assert_eq!(x, 0.0);
-    assert!(1.0 / x < -2.0); // this works fine
+    assert!(1.0 / x < -2.0); // this still works fine

(Playground)

1 Like

I've far more often seen nonsense "close to" tests using arbitrary epsilon values (sometimes ULP/epsilon, which is just about always wrong!) that introduce worse problems than the test was solving, if any: often it's just the cargo cult "you can't compare floats" "knowledge", which as others here have said is not true at all.

People aren't that confused when integers have 5 / 2 * 2 != 5, which is the same thing, so just saying "fractions can round" is more than enough warning.

6 Likes

The float_eq create has options for every choice of epsilon that makes sense!

// Integer floats: exact result
assert_float_eq!(got, 274689, abs <= 0.0);
1 Like