I know of three functions for checking for equality: ==, ===, and isequal. The documentation comments on subtlety regarding floating point numbers, and there is indeed a difference:
julia> NaN == NaN
false
julia> isequal(NaN, NaN)
true
julia> NaN === NaN
true
Could you explain why they behave this way? I am particularly interested in
Why isequal was designed to behave differently from == for float numbers.
Why NaN === NaN is true. For most other types, I would have naively assumed a === b implies a==b since x===y
Determine whether x and y are identical, in the sense that no program could distinguish them.
according to the documentation.
In terms of my knowledge level, I know floating point numbers are represented as bitstrings with a sign bit, exponent bits, and mantissa bits, but don’t really know much else.
How NaN are handled is outlined in IEEE-754. All comparisons between NaN and any number including itself return false.
=== on the other hand is a different operation that checks that no operation could tell them apart. For mutable structs this is pointer equality and for immutable structs this is bitwise equality.
There are actually several different representations for NaN’s, which are not ===. The best way to check if a value is NaN is with the function isnan
Thanks for pointing me to IEEE-754. I’ve always thought of == as checking for equality of value, and did not know there was a specification given by IEEE. Now it makes sense that NaN==NaN is false but Nan === Nan.
Do you also happen to know why isequal was designed to return true for two NaN in Julia?
As @WschW notes, there is only one best practice when it comes to determining if one or more values is “not a number” (NaN): use isnan(x) or, to determine if two values are NaNs, use e.g. nans(x,y) = isnan(x) && isnan(y) . And, fortunately, isnan(x) runs fast.
The representation of NaNs specified by the standard has some unspecified bits that could be used to encode the type or source of error; but there is no standard for that encoding.
So it’s not just that we don’t print the sign of NaN. There is the 52 bits of payload that we also don’t print. The reason we don’t print them is that they really don’t matter (for mathematical purposes). Arithmetic operation on a NaN will return the “same” result (modulo the sign and payload of a NaN, assuming the result is NaN) regardless of what NaN is passed in. There are some non-arithmetic functions (such as signbit(NaN), copysign(1.0,NaN)) where the sign of the NaN may affect the result.
There are 2^53-2 distinct representations of 64-bit NaN (2 different signs, 2^52-1 different significands since one is used by Inf). Each will only be === with exactly itself (=== requires the bit patterns to be identical). Note, also due to IEEE754 semantics, that -0.0 == 0.0 (but still -0.0 !== 0.0). As a bit of trivia, == and === are equivalent for IEEE floats so long as at least one argument is neither -0.0, +0.0, or a NaN.
Note that I don’t think Julia (or your hardware, which Julia defers to in most cases) usually produces 64-bit NaNs other than 0x7ff8000000000000 and 0xfff8000000000000 by “natural” means. But you certainly can introduce them via things like reinterpret(Float64,-1).
IEEE754 has nothing to say about ===. It’s a different construct for an entirely different purpose than anything IEEE754 cares about. Without ===, NaNs are pretty much interchangeable modulo the effect of the sign bit on some operations.
You also have (so rather use isequal and !isequal):
julia> NaN32 !== NaN # There's also NaN16 for Float16, and there are different such e.g. bfloat, and conceivably NaN8, Float8 is available in a package I believe, also Posits have NaR.
true
while:
julia> NaN !== NaN
false