MeToo came here to understand the reasoning, why NaN == NaN
equals false.
After reading (nearly) all I still was puzzled, why a == NaN
cannot replace a function like isNaN()
, because it seems to be so obvious.
But things are not that simple.
Nobody has mentioned vector geometry yet. But many computations take place in the 2nd or 3rd dimension, so in vector space.
After thinking about this a bit, I immediately realized, why it is a good thing to have NaN
not to compare to itself. Following hopefully is easy enough to understand for others, too.
Vectors
Bear with me, it takes a while until NaN
shows up.
First let me explain a bit for people who are not deep inside math
In vector geometry we usually use something like complex numbers.
A complex number is made of two floats a + bi
(where i
denotes the imaginary value with i * i == -1
) which allow us to address all points on the 2 dimensional plane. With floating point we cannot express each value, so we have to approximate a bit. So if we round the values to some value we can express, we can still try to create numerically stable algorithms, which give us some good approximation of what we want to archive.
Enter infinity
No NaN
here yet. Please be patient. I'll get to the point later down below.
If we want to specify some point far far away, we might leave the range of numbers we can express, which results in infinity. In IEEE floats we luckily have +inf
(I write it as inf
) or -inf
for this (written as -inf
).
This is good:
a + inf i
makes sense, right? It is the vector to some point on the x-axes at location a
and on the y-axes at location "positive infinity". But wait a bit, we are talking vectors here!
Vectors have an origin and a point they point to. Normalized vectors are those, which start at location (0,0)
.
Now think of a vector with origin of (0,0)
which points to (a,inf)
.
Still makes sense? Not quite. As we look a bit closer, we will see, that the normalized vector (0,inf)
is the same vector! As the vector is so long, the derivation of a
in the infinty can no more be seen. Or said otherwise:
For infinitively long vectors in the cartesian coordinate system, the finite axis can be expressed as 0
, because we are allowed to approximate (if we are not allowed to approximate, we cannot use floating point!).
So the replacement-vector (0,inf)
is still suitable. In fact, any (x,inf)
is a suitable replacement for a finite x
. So why not use 0
from our origin of our normalized vector.
Hence what do we get here? Well, with allowing inf
in our vectors, we actually get 8 possible infinite vectors, each 45 degrees rotated (degrees in parentheses):
(inf,0)
(0), (inf,inf)
(45), (0,inf)
(90), (-inf,inf)
(135), (-inf,0)
(180), (-inf,-inf)
(225), (0,-inf)
(270) and (inf,-inf)
(315)
All this does not cause any trouble. In fact, it is good to be able to express more than just finite vectors. This way we have a natural extension of our model.
Polar coordinates
Still no NaN
here, but we are getting closer
Above we used complex numbers as cartesian coordinates. But complex numbers also have a 2nd option how we can write them. That is polar coordinates.
Polar coordinates are made up of a length and an angle like [angle,length]
. So if we transform our complex number into polar coordinates, we will see, that we can express a bit more than just 8 angles in [angle,inf]
.
Hence, if you want to create a mathematical model which allows infinitely long vectors in some multidimensional space, you definitively want to use polar coordinates in your calculation as much as you can.
All you have to do for this is to convert the cartesian coordinates into the polar ones and vice versa.
How to do this is left as exercise for the reader.
Enter NaN
Now, what do we have?
- We have a mathematical model which calculates with polar coordinates.
- And we have some output device, which uses cartesian coordinates, probably.
What we now want to do is to be able to convert between those two. What do we need for this?
We need floating point, of course!
And as we perhaps need to calculate with some few terabillion coordinates, (perhaps we render some weather forecast or have some collision data from the large hadron collider) we do not want to include slow and error prone error processing (WTF? Error prone error processing? You bet!) in all those complex mathematical (hopefully numerically stable) steps.
How do we propagate errors then?
Well, as said by IEEE: We use NaN
for error propagation
So what we have up to here?
- Some calculation in the polar coordinate space
- Some conversion into cartesian space
- NaN as rescue if something fails
And this then leads to ..
.. why NaN == NaN
must be false
To explain this, let's reduce this complex stuff above all to a simple result of 2 vectors in cartesian coordinates:
And we want to compare those two. This is how this comparison looks like:
Everything correct so far?
Yes. But only until we observe following two polar vectors which might be the source of our two cartesian vectors:
Certainly those two vectors are not equal in the polar coordinate space. But after conversion into cartesian space, both come out as:
Well, should they suddenly compare equal?
Surely not!
Thanks to IEEE defining that NaN == NaN
must return false
, our very primitive vector comparison still gives us the expected result!
And I think, that exactly is the motivation behind, why IEEE defined it as it is.
Now we have to live with this mess. But is it a mess, indeed? I'm undecided. But, at least, I now can understand the (probable) reasoning.
Hopefully I did not miss something.
Some last words
The primitive way of comparing things usually is not fully appropriate when it comes to floating point numbers.
In floating point, you usually do not use ==
, you rather use something like abs(a-b) < eps
with eps
being some very small value. This is because already something like 1/3 + 1/3 * 2.0 == 1.0
might not be true, depending on which hardware you run.
1/3 + 1/3 * 2.0 == 1/3 + 1/3 + 1/3
should be true on all reasonable hardware. So even ==
can be used. Only carefully. But is not ruled out.
However this does not render above reasoning void. Because above is not a mathematically proof for that the IEEE is right. It is just an example, which should allow to understand the source of the reasoning behind, and why it is probably better to have it defined the way it is.
Even that it is a PITA for all programming people like me.
while (fabs(x - oldX) > threshold)
, exiting the loop if convergence happens or a NaN enters the computation. Detection of the NaN and appropriate remedy would then happen outside the loop. – Avaricious