Hacker News new | past | comments | ask | show | jobs | submit login
NansAreNumbers – An esoteric data type built of NaNs (github.com/thoppe)
90 points by shelfchair on Sept 29, 2021 | hide | past | favorite | 54 comments



It’s much more interesting to use the mantissa bits to store data than using a NaN as a very big bit.

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.

[0]: https://github.com/philihp/nanbox


This is just begging for a malware command&control system for SCADA controllers based on embedding commands and responses in the raw float values of some sensor data streams..


In the readme, at the end, there is this code sample:

# [[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.


Reminds me of the work done by Tom7 for Sigbovik https://youtu.be/5TFDG-y-EHs


You go away. You go away right now and you think about what you’ve done.


If they didn't do it, someone else might. Isn't that how everything gets justified?


It sort of works as an argument if they also supply a "an we do it in this more positive way than others would" bit to the statment. But that's usually left out in silent acknowledgement that the person speaking actually is acting unethically because they don't provide any positive effect other parties wouldn't.


Obligatory xkcd: https://xkcd.com/242/


nice!. one thing about NaN is that, because the specification only mentions the exponent part, you have all the mantissa bits free for use, this is the idea behind signalling NaNs, that if you return a NaN for an operation, that value should have some helpful debug information


Also, NaN boxing. Which is (was?) used by JavaScript Core (Safari’s JS engine).

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.


Hopefully not as an actual double, right? Not that many floating point registers compared to integer on most archs.


There are an equal amount of vector (also used for FP) regs as there are gprs on x86_64 and on Aarch64. Since those registers are rarely used in normal general purpose code, moving some values over there in principle saves register space.

(x87 fp is obsolete and should never be used for anything.)


I'm not sure the latency on moving data between SSE and GP registers on modern x86 processors. If I remember correctly, late 1990s x86 was notoriously slow at moving data between GP registers and the x87 fp stack. I could certainly believe that on some architectures, moving data between vector registers and GP registers is slower than an L1 cache access.


I think you can use general purpose registers so long as you don’t actually need to do IEEE 754 math. I’m not clear on the details of how the byte code/JIT deals with it, my only experience is with hire values are stored in the heap.


Yes, they’re stored in a union and passed around as whatever is convenient.


It’s still in use, plus Mozilla uses it too.


What has always annoyed me is that the spec never says, AFAIK, if the 23rd bit should be ON vs OFF for a signalling vs quiet NaN, which means that you can't portably set a value to a quiet NaN at compile-time.

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.


All but the first one (MSB) which, while not specified, is sometimes used by systems as a flag for loud errors vs. quiet errors.


A statement I once heard, attributed to Christopher Strachey: “Merely because you can push a pea up a mountain with your nose doesn't mean you should.”

(I heard this from Joe Stoy, who worked with Strachey, so I suspect it's authentic.)


How do you represent negative NaN in IEEE 754?


what about the payload bits?


You had me at NaN.


I watched "The Flying NaN" as a kid.


In JavaScript:

(!NaN) // true

(NaN == false) // false


I think that says more about JavaScript than NaN.

Also JavaScript:

"10"+1 => "101"

"10"-1 => 9


The hell?


String addition = concatenation

String subtraction = ??? => Number subtraction


    typeof(NaN) // 'number'


yes. it is nota number. the result of taking the square root of a negative result from dividing by zero.


Is this interesting? It's true for undefined, null, empty string, 0, maybe empty arrays sometimes, ...


Yes, what you presented are WTFs. But the biggest WTF is that in every language, NaN == NaN yields false.


That makes perfect sense. Why should two NaNs compare equal? NaN is Not A Number - it means "you messed up and did an invalid calculation". You can't do equality comparisons on that.


Yet null is considered "not an object", but every language other than SQL implements null == null as true.

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.


NaN doesn't represent a number that doesn't exist, it represents a value that hasn't been computed because the computation yielded an error which wasn't handled yet.

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.


Null and nullability in general is also a mistake that many languages carried over from C where I think it originated (though it possibly predated C). So it doesn't surprise me that there are other problems with null, like for example, comparing two nulls.


Removing nullability of a reference doesn’t mean removing null-relationships, it means properly typing them as Maybe/Option sum type to forcibly diverge code paths that work with non-existence cases. A language without an advanced compile-time type system cannot have these, not to mention untyped languages. You can’t just remove null/none/nil/undef from js, python, etc.

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.


But this is orthogonal to the equality property: the commkn alternative to using null/nil values is to use algebraic types (e.g. Option<T>), and with them too None == None is true


Because one of the basic axioms of equivalence relations (which equality should certainly be) is reflexivity; anything should be equal to itself, and other code will break if it isn't (e.g. in many languages you can corrupt a binary tree structure by inserting multiple NaNs into it, because they'll both compare less than each other).

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.


> Because one of the basic axioms of equivalence relations (which equality should certainly be) is reflexivity; anything should be equal to itself, and other code will break if it isn't (e.g. in many languages you can corrupt a binary tree structure by inserting multiple NaNs into it, because they'll both compare less than each other).

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.


> 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.

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.


> if you want a sum type, define an actual sum type

> 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 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.


> 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.


Using the same symbol for two arbitrary, unrelated things is confusing for the reader - especially if you're using a symbol and a term that already have a standard mathematical meaning. 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). And this means you can write code that uses that + operator in a generic way and be able to rely on it making sense - e.g. if you write a "sum of a list" function, and then refactor it to sum the lists in parallel, if your refactoring was correct for lists of numbers it will be correct for lists of strings as well.

(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.


> "==" 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.


> I would expect that "the reader" with "usual expectations" would be familiar with floating point operations

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.


The whole point of NaN working this way is that it is not well behaved and raises an alarm when a bad computation is made. Otherwise you run the risk of wiping out a NaN and never knowing that a divide by zero has corrupted your results.


Having it not compare equal to itself is a pretty crappy way of making it raise an alarm - you're relying on that accidentally causing some kind of visible corruption to your program's datastructures, which is not at all guaranteed. Anyone who cares about not silently getting corrupt results should be turning on floating-point exceptions rather than using NaNs, unless they know exactly what they're doing.


You seem to be equating having a NaN in a result as being undetectable corruption. The whole point of NaN is that you don't end up with silent data corruption, you end up with an unusable value: unusable in the sense that it infects further calculations instead of covering it up with a value that looks valid.

    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.


> The whole point of NaN is that you don't end up with silent data corruption, you end up with an unusable value: unusable in the sense that it infects further calculations instead of covering it up with a value that looks valid.

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.


NaN as an approach to handling arithmetic errors only makes sense (weighed against checked arithmetic) if it poisons all computations involving it.

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).


Yeah, and even in the basic case of infinities/limits, it's pretty obvious that they're not equal. Otherwise you could make any equation work by dividing both sides by zero.


That would come directly from IEEE754:

> Every NaN shall compare unordered with everything, including itself.

(5.11 Details of comparison predicates)


It’s weird, but it exists to let you check for the existence of NaN.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: