Inconsistency of NaN

0/0 returns NaN
NaN*false returns 0.0
(0/0)*false returns -0.0
Isn’t this an inconsistency of NaN? :thinking:
Also:
julia> (NaN*(0/0))*false
0.0
but:
julia> ((0/0)*NaN)*false
-0.0
:thinking:

1 Like

It appears that these are two different bit values although they both print as NaN

julia> reinterpret(UInt,0/0)
0xfff8000000000000

julia> reinterpret(UInt,NaN)
0x7ff8000000000000

julia> 0/0 == NaN
false

they differ only by a single bit, presumably the sign bit

1 Like

Ok, but:
julia> 0.0 == -0.0
true
:grimacing:
though:
julia> 0.0 === -0.0
false

so at least

julia> (0/0)*false == NaN*false
true
1 Like

That’s correct, they can’t be egal (===) because the sign bit is different:

julia> reinterpret(UInt, 0.0)
0x0000000000000000

julia> reinterpret(UInt, -0.0)
0x8000000000000000

or

julia> bitstring(0.0)
"0000000000000000000000000000000000000000000000000000000000000000"

julia> bitstring(-0.0)
"1000000000000000000000000000000000000000000000000000000000000000"
3 Likes

Ok, and what about:
julia> (NaN*(0/0))*false
0.0
but:
julia> ((0/0)*NaN)*false
-0.0
:thinking:

In IEEE-754, the IEEE standard for floating point arithmetic, which Julia and most other languages use, NaNs are encoded with the exponent field filled with ones. According to the standard, the sign bit does not matter for determining NaNness, and the sign bit is precisely what’s different between 0/0 and NaN. I’m not sure exactly why 0/0 results in a different sign bit, but it could well just be an implementation detail. Next, there’s the multiplication with the Bool false, which (eventually) hits this method:

The copysign is what causes the sign bit of the NaN to appear in the final result. I’m not sure whether that’s intended. I suppose it is a little weird that information that’s supposed to be (mostly) immaterial has an observable effect here. Then again, it seems harmless.

5 Likes

Why is why the way to determine NaNness is to use isnan and not any number of consecutive ='s. You’re not supposed to do 0/0 == NaN to check if the lhs is NaN.

2 Likes

or use haskey

julia> haskey(Dict(NaN => nothing), 0/0)
true

There are several things to unpack in this post.

  1. false*NaN is special cased to make false “strong”, see the implementation

  2. but the copysign method for NaNs is a bit misleading, as NaNs are not supposed to have a sign:

    julia> sign(0/0)
    NaN
    
    julia> copysign(1.0, 0/0)
    -1.0
    

    I don’t know the arcane details of IEEE 754 about whether this is the intended behavior. In any case, it is mostly innocuous.

  3. (NaN*(0/0))*false is the same phenomenon, except the sign bit comes from the first operand.

3 Likes

It’s what the hardware does, iirc.

3 Likes

Or rather isequal (which is what haskey uses): isequal(NaN, 0/0) == true.

3 Likes

Whoa there. I don’t know if these are officially bugs but they are at odds with the IEEE 754 spec, which reads in part:

For an operation with quiet NaN inputs, other than maximum and minimum operations, if a floating-point result is to be delivered the result shall be a quiet NaN which should be one of the input NaNs.

See here: http://irem.univ-reunion.fr/IMG/pdf/ieee-754-2008.pdf

I understand why people usually want false*x to be falsy but NaN is falsy enough and more correct than 0.0 for false*NaN.

2 Likes

I agree. But since this has been in the language for a while, I think a lot of programmers (myself included) use false*x on uninitialized data and expect to get rid of NaNs that way.

I think it would be nice if Julia had a “strong zero” in a type of its own. I even wrote a package implementing that a while back (though I never registered it or upgraded it to be 1.0 compatible.)

2 Likes

I think the argument is that IEEE-754 does not pertain to operations involving boolean inputs. I’m not picking sides on whether or not the current behavior of various operations with mixed floating point number and boolean inputs is ‘right’, but it’s not a clear-cut case.

1 Like