NansAreNumbers – An esoteric data type built of NaNs
github.com
github.com
From nanbox [0] README:
> IEEE 754 encodes 32-bit floating points with 1 bit for the sign, 8 bits for the exponent, and 23 bits for the number part (mantissa). For the specific case of NaN (e.g. the result of dividing 0 by 0, or the square root of a negative number), the spec encodes this as 11111111 in the exponent. The sign and the mantissa can be anything, and the spec suggests this can be used for "diagnostic information". One/some of these bits is/are commonly used to indicate a quiet NaN (qNaN) which could be expected vs. a signaling NaN (sNaN) which could be unexpected and should trigger an halting exception. This behavior is however sometimes different on different hardware.
> 23 bits isn't much space, but conveniently the max size of a Unicode codepoint is 0x10FFFF, making it a 22-bit charset. When you encode your strings in UTF-32, you're only going to be using 22 of those bits, so masking the top portion with the NaN signature makes all of your characters NaNs.
# [[nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, (...) nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan]]
After reading this snippet, you, too, might have for the day, or week(s) the soung stick in your head. NyaN. sorry, not sorry.
All values are stored as a double; true, false, null, undefined have different NaN values. NaN even has enough spare bits to store Object pointers. Short strings are also stored packed into NaN bits.
(x87 fp is obsolete and should never be used for anything.)
SGI/Irix machines in the 2000s, for example, had perfectly good floating point but had the 23rd being OFF for a quiet NaN, while other machines have it ON for quiet NaN. And this is independent of endien-ness: these SGIs were big endien just like other big-endian machines that made quiet NaN differently.
(I heard this from Joe Stoy, who worked with Strachey, so I suspect it's authentic.)
(!NaN) // true
(NaN == false) // false
Also JavaScript:
"10"+1 => "101"
"10"-1 => 9
String subtraction = ??? => Number subtraction
typeof(NaN) // 'number'Because x == x is falsified by NaN, it's not obvious to a human auditor or compiler optimization writer than you can't just blindly fold this expression without being very careful about data types.
I would sum it up by saying that x == x yielding false violates the principle of least surprise. It'll trip up every beginner who hasn't been explicitly been taught about NaN's behavior.
Imagine having error values (or exception objects) that get allocated every time an error happens. You don't expect necessary to be able to compare two instances of an exception object, instead you'll call a function that will tell you e.g. which type of error it is.
Now, NaN are not really quite like that. For one, it's unusual to stuff the error object in the same place as the actual value and also treating it as if it was the same type as the value. Furthermore reassigning the error object into another variable implicitly "clones" it into a new identity.
It's possible to imagine an alternate universe where each arithmetic operation that produces a NaN allocates a new unique number (there are many available bits so that you'll get quite far without a wrap around. But the additional complexity in hw was deeped not worth it.
NaN self-inequality comes from the old FPU design decision (FCOM/FUCOM instruction group), which is probably questionable (or rational if you dig deeper) in regard of a programmer’s convenience. That carried over to all languages because it’s a fundamental hardware behavior that cannot be fixed without an overhead.
Maybe there are legitimate use cases for that kind of comparison, but you shouldn't use the conventional == operator for it. I think OCaml avoids the problem by having separate operators for floats, and Rust sort-of does by having a distinction between PartialEq and Eq.
NaN is not something that can be pointed to. NaN doesn't exist like numbers exist in relation to each other, it's a concept (or at least more conceptual than numbers themselves are conceptual). NaN doesn't exist on a number line, so it makes no sense to order, compare for greater or less than, a NAN with numbers or anything else. It literally means "this doesn't exist". It doesn't have a "self" for reflexivity to apply to. A binary tree's nodes need to be comparable and orderable, and NaN is not comparable to anything. There is no place to put a NaN in a binary tree, nor is there really a use case to "find a NaN quickly" that a binary tree would be useful for (what would the utility be of a binary tree where the keys are all NaN?). NaN is a sentinel, and it exists outside the set of floats, despite being a float itself for type purposes, which is why it needs to be tested for using specific operators (isNaN) and not using equality operators. Part of the utility of NaN is that it can not be tested for using equality.
Of course, computers are limited in what can be represented using bits, so NaN does have a specific (set of) binary representation(s), and you could work with those, but that avoids the useful abstraction NaN is mean to represent.
That seems like pretty bad type design - if you want a sum type, define an actual sum type. If a given type doesn't have a well-behaved concept of equality or comparison (and to be clear, that means well behaved for any values of that type), it shouldn't support the standard equality or comparison operators. I agree that you probably don't want to put NaN in a binary tree, but the language should support you in not doing that rather than silently breaking when you do.
> the language should support you in not doing that rather than silently breaking when you do.
You do realize that we're not talking about a programming language and its type system, right? Nothing is stopping anyone from creating such a sum type in the languages that support such. Places that don't support rich type systems, such as C and even assembly, need to be able to support floating point semantics. That doesn't change the fact that in such a system, the NaN type will, by definition, define a comparison operator that always returns false and expose some other means to determine if a value is a NaN.
That's a language design decision. You don't have to define that kind of comparison operator. You certainly don't have to make the standard == behave that way.
Is equality an attribute of the operator or an attribute of the type and value? The fact that one defines operators on types—and that things such as how values compare to each other is based on types and their coercion rules when compared (str==num→false, unless you define how a num becomes a str or a str becomes a number, which is type based)—says to me that it's the latter.
(If your language doesn't allow polymorphism, you might have to write the same code twice but you'd be able to use the same symbol, which helps a reader understand how it's conceptually the same thing)
"==" for two different types won't do exactly the same thing, but it should always be a well-behaved "equals" operation that satisfies the usual expectations that a reader would have. That usually includes that any value is equal to itself.
NaN is not the same kind of value that any number, say 3.14, is. I would expect that "the reader" with "usual expectations" would be familiar with floating point operations, how NaN works when it comes to equality is well defined, as well defined as equality over integers.
I'm not sure what point you're trying to make. That "equality" as embodied in the operator "=="? But further up the thread I talk about how equality, and other operators, is something that is defined on/by the types, you admit as much when you say:
> str + str for concatenation might seem unrelated to num + num, but it's a valid "+" in the standard mathematical sense of being a valid monoid operation (i.e. it's associative).
How operators work is an attribute of the types the operators work on. str defines the + operator to mean concatenation, which is entirely different from the meaning of the + operator when applied to integers, or what should be expected to happen when the LHS and the RHS of the binary + operator are of different types (some choose to do fail without explicit conversions, others choose to implicitly convert or promote).
isNaN is used to determine if the bitpattern in a floating point value is NaN because equality on the bitpattern that is floating point is defined very specifically for those bitpatterns. The same could be said for a hypothetical isPi function over the floating point domain, which could be defined to return true for any value that approximates π, which makes sense because the exact value of π can not be represented in binary or in decimal. Being an irrational number, equality wouldn't work on a floating point representation/approximation of π either, so a separate operator (operator is just another name for function) would be necessary in order to determine if a given value was Pi-approximate.
Why? Why should every single programmer be expected to be an expert in this particular obscure datatype, to the point that it's ok for it to break all the normal rules that normal datatypes follow?
> How operators work is an attribute of the types the operators work on. str defines the + operator to mean concatenation, which is entirely different from the meaning of the + operator when applied to integers
It's not "entirely different". It conforms to the normal mathematical definition of + and has the properties that a reader would expect + to have (e.g. associativity). Defining to e.g. search strings would be bad and confusing.
r = (x / y) + 1
If y was 0, I wouldn't want r to equal 1; if r was 1 (or anything other than NaN, really) then that would be silent data corruption.That some languages have a literal form of NaN that can be typed into code and that has a type that is the same as the type of floating point numbers leads one to believe that an equality operator can/should be used to test for NaN. Really, a literal form of NaN is kind of a blight on such a language: the only way to obtain a value that is NaN should be via a calculation that results in it, there is very little need for a literal form, except for the case where you want to store the result of a calculation that might be NaN (to record that the calculation was corrupted). But in the wide range of cases, if someone was typing a literal NaN into their code, that would be a code smell: NaN is a legit result of a calculation, but not a legit input to a calculation because it infects any calculation it touches, and since there's not a legit reason to have it as an input, there's no reason to have a literal form that can be typed into the code.
As for floating point exceptions vs testing for NaN:
x = performcalculation()
if isNan(x) { abort }
y = performFurtherCalc(x)
if isNan(y) { abort }
This can be done at every point an additional value is introduced to the calculation, to determine if the result is sane or not. Floating point exceptions do these checks for you.NaN values are slightly better than silently returning a valid value like 0, agreed. But having NaN silently corrupt a datastructure when you insert it into it is not what anyone asked for. Either make NaN a well-behaved value with well-behaved equality, or make it immediately explode like an exception.
> As for floating point exceptions vs testing for NaN:
x = performcalculation()
if isNan(x) { abort }
y = performFurtherCalc(x)
if isNan(y) { abort }
That's horrible ergonomics, that's like something from the '70s. I know there are still languages that lack proper sum types and high-level composition, but any serious language that can't solve that properly at least has some kind of language-level hack like exceptions.I would argue firstly that floating-point numbers should not implement float == float → boolean comparison, because downgrading NaN to a boolean loses its infectious nature, undermining the purpose of NaN as an error-handling (or at least -detection) scheme.
But if you’re going to implement float == float → boolean comparison, I would argue that NaN not equalling itself is more reasonable on this axis of poisoning all computations than the alternative, though I will admit that it’s rendered subjective by the fact that there is no inherent virtue in equality (your “normal” path could just as easily be conditional on inequality).
I’d really like to see more experimentation into languages that only support checked arithmetic (not just for floating-point numbers, but also for integer types).
> Every NaN shall compare unordered with everything, including itself.
(5.11 Details of comparison predicates)