Several threads here have discussed how f64
doesn't implement Eq
or Ord
or Hash
because of NaN values. This is indeed unpleasant.
I think of NaNs as a floating point arithmetic design mistake, similar to having null
values by default in reference types, which Tony Hoare has called a "billion dollar mistake".
But here is another problem with NaNs. Consider this innocent-looking code:
println!("{}", f64::sqrt(1001.3 - 1001.0 - 0.3));
You would think it would print something in the vicinity of the correct answer, 0.0, maybe a bit off due to rounding errors.
What does it actually print? NaN.
That's because due to rounding errors it ends up taking a square root of a tiny negative number, and sqrt is defined to return NaN for negative numbers.
Therefore really, instead of f64::sqrt(x)
, you should always do f64::sqrt(x.max(0.0))
, if you want to be robust to rounding errors.
I think it would be a lot better if sqrt
was simply defined to be 0 for negative numbers. Unfortunately the IEEE-754 standard chose this to be NaN instead.
Similarly, f64::acos(1.00001)
is NaN. You should really do f64::acos(x.clamp(-1.0, 1.0))
if you want to be robust.
Same thing for f64::asin
, f64::atan
, f64::log
, etc. You really have to clamp the input to the correct range if you want them to work robustly in the presence of rounding errors.
I can think of only a few scenarios where there is no clear better alternative to NaN: 0 / 0, 0 * ∞, ∞ - ∞. It's not clear what they should return.
But, I think, it doesn't really matter very much what they return. If your code is robust to rounding errors, it has to be prepared for any answer to 0 / 0. If it's 0 / 0 precisely, you get NaN. But if it's 1e-300 / 0, you get infinity. If it's 1e-300 / 1e-300, you get 1.0. Etc. This is just a very unstable calculation.
So I think NaN might make sense for a few special cases like this, but not for other cases like sqrt(negative) or log(negative).
Since these special cases are only a few cases, and your code usually has to be prepared to get any answer from them anyway, it wouldn't do much harm to have a floating point arithmetic that just returns an arbitrary non-NaN answer in those cases as well (such as 0 or infinity), and get rid of NaNs altogether.
Also note that 1.0 / 0.0 = infinity
is already quite arbitrary (it could be -infinity
). You could argue that it's +0.0
and not -0.0
, and 1.0 / (-0.0) = -infinity
. But if we go that rabbit hole, then, 0.0 - 0.0 = +0.0
is an arbitrary sign choice, why not -0.0
? So this business of dividing by zero is already quite arbitrary. So might as well make +0.0 / +0.0 = infinity
and -0.0 / +0.0 = -infinity
and it would be pretty consistent with all the other arbitrary choices about +0.0 and -0.0.