two case studies of NaN
27 points by seb
27 points by seb
I love things like this that repro with a few lines of code and are fundamental.
In terms of sociology, I suspect if your inputs hit these values you add guards for them over time (and probably get good at preemptive guards and tests as you gain experience)
I wonder if not having the guards in the language implementation was a conscious decision (leave it as a user space decision) now lost to time.
To give a bit of context, NaN != NaN is an artifact from back when IEEE 754 was very young and the standard did not yet specify an isnan() function. Instead, one was supposed to identify a NaN by comparing it to itself.
In the meantime, IEEE 754 has included a so-called total order predicate, an alternative ordering. Using this predicate, NaN == NaN holds, and I think it's a sane choice.
to be clear, i'm not necessarily saying that python's behavior here is wrong, or that this optimization is bad because NaN behaves weirdly.
Why not? It clearly is wrong. Maybe that wrongness is considered acceptable, but I don’t think you could reasonably argue that it isn’t wrong.
I guess it depends on how you define "wrong". To me, "wrong" implies that there's a bug, but there is no bug; it's just a consequence of a design decision.
What I moreso meant was that I wasn't trying to imply that Python shouldn't perform this optimization and should always check list members for true equality, just because of a special case which likely doesn't matter in practice. Not that I think it should; just that I'm not taking a stance either way. I just wanted to document some mildly cursed behavior which I found amusing :3
And like, ultimately, the rules are all arbitrary. If you define list equality as first checking comparing the members' identities, then [nan] == [nan] is correct, since it matches the definition. The same reasoning for [nan] == [nan] being "wrong" can also be used to argue that nan != nan is "wrong". After all, it's a value which isn't equal to itself! That's absurd, who in their right mind would call that correct?
I don’t know what the most authoritative source is, as I don’t believe there’s any formal specification; but by the regular docs, the behaviour is what I would call a documented bug.
Sequences compare lexicographically using comparison of corresponding elements.
For two collections to compare equal, they must be of the same type, have the same length, and each pair of corresponding elements must compare equal
Cut and dried: [nan] == [nan] is wrong. Incidentally:
>>> [nan] == [nan]
True
>>> [float('nan')] == [nan]
False
But:
The built-in containers typically assume identical objects are equal to themselves. That lets them bypass equality tests for identical objects to improve performance and to maintain their internal invariants.
That assumption is manifestly invalid. That makes it what I’d call a documented bug.
(Incidentally, I can’t imagine what the “internal invariants” would be.)
Isn't this a part of (my personally-hated) IEEE-754, and not IEEE-759 as it is documented there?
<opinion/diatribe>
IEEE754 may be fast due to hardware support, but it is also a heaping pile of shite. Nondeterministic computations (one of the worst sins in all of computing), counterintuitive behavior (0.1 + 0.2 not equaling 0.3, for example... And yes, I understand why, but you're completely missing the point if you reach for that)
On the bright side, it is fast (again, thanks only to hardware support) and represents conceptual values like Infinity, which is useful.
But unums, posits, bignum rationals, and bignum fixpoint decimal are all way the hell better solutions for the vast majority of cases, ESPECIALLY regarding financial computations, IMHO.
https://gist.github.com/pmarreck/78b6d0f68254b2daaa3fc88d7dda2ebf
</opinion/diatribe>
Bignum rationals and fixpoint were never a consideration when machines where in the megahertz range (and it was in single megahertz when IEEE-754 was designed). Historically, every specialized computer (like Crays, or the various Lisp machines) have always lost out to general purpose computers. The specialized computers were way more expensive, and was harder to improve speed over the generalized computers.
Unums and posits are new (within the past fifteen years or so) and while they may be better, you can't go back to the late 70s/early 80s and push for their implementation. I've worked with non-IEEE-754 floats (used before IEEE-754) and ... IEEE-754 is strictly better (in my opinion). No infinity, hard exceptions where IEEE-754 uses NaNs, and you can get consistent results across different machines that use it (while 0.1+0.2 does not exactly equal 0.3, you do get the same results on any IEEE-754 based system).
So
But yes, using something that manipulate ranges of numbers instead of fixed numbers is not usual. Saying it is bad is not helpful
It is not a real problem, the math is correct.
Can you expand on this? Are you saying that if it is defined to work a certain way, it can't be questioned? What does 'real' mean to you?
That is a good question! What is maths and reality, all that jazz right?
Ok let's do it this way. 0.2+0.1 being 0.3 depends on defining at least addition. We know, because we overload symbols all the time in software.
In this case, the addition is not even overloaded. What is the problem here then?
Well it is the choice of how to represent floating point in text to humans. .1, .2, .3 are shorthand in floating-point to represent a range. And we do not pick the middle of the range. We pick the most readable and shortest number. Both to compress and to make it easier to reason.
So how is it a "real" problem only if we decide to make it one. It is not going to breal anything.
Isn't this a part of (my personally-hated) IEEE-754, and not IEEE-759 as it is documented there?
oops yeah that was a typo. fixed :)
Amusingly it's the IEEE Standard Test Procedures for Semiconductor X-Ray Energy Spectrometers. I'd be pretty impressed if you'd managed to connect the two in the article :)
NaN is wrong. It's a footgun, so I left it out of my programming language. If a floating point operation fails, then instead of returning NaN, the operation fails, which is similar to throwing an exception. This makes floating point operations behave consistently with every other operation in my language that can fail.
This change has been helpful to me, since I get a stack trace at the exact spot where a floating point operation fails, allowing me to more easily identify and fix bugs. I don’t miss NaN, there has never been a circumstance where I wished to go back to the old behaviour.
If you don't allow NaN, equality is an equivalence relation, which is necessary for reasoning about code. And the other problems mentioned in the article are also fixed.
Tony Hoare, who invented the NULL pointer in 1965, later called it his “billion dollar mistake”. Today, everybody recognizes this, and recent languages have nullable or optional types, instead of including NULL in every pointer type. NaN is the NULL of the floating point world. Let’s recognize that.