Negative false no longer acts as a strong zero

Julia documentation states that false acts like a strong zero in multiplication, such that NaN * false produces 0.0. However, Julia also defines -(x::Bool) = -Int(x), which seems to indicate -false is no longer a strong zero.

Personally, I find it a bit confusing, as:

julia> -NaN * false
-0.0

julia> NaN * -false
NaN

and

julia> 1 - NaN * false
1.0

julia> 1 - false * NaN
1.0

julia> - false * NaN + 1
NaN

julia> - NaN * false + 1
1.0

IMHO, this behavior may lead to some inconsistency and unpredictability. For example, as mentioned in `rmul!` does not respect "strong zero": `NaN*false=0.0` Ā· Issue #2607 Ā· JuliaGPU/CUDA.jl Ā· GitHub,

julia> using LinearAlgebra

julia> rotate!([NaN], [NaN], false, false) # rotate!(x, y, c, s) means: x = c*x + s*y and y = -conj(s)*x + c*y
([0.0], [NaN])
8 Likes

I’m not sure I agree that’s really a problem. false is a strong zero. -false is a different value of a different type. Why would that be a strong zero? Isn’t that exactly like how false + 1 is not a strong zero?

3 Likes

But is -false not more like -1 * false? Which then should be a strong zero. Or at least that I would think of it more like that than 0-false, though maybe that is not warranted.

1 Like

Why would -1 * false be a strong zero? It’s not even a zero? It’s a different type.

1 Like

One thing is -false, which could, arguably, in isolation, be equal to false, (just like -0 == 0) and thus a strong zero.

But what should -true be then?

1 Like

It’s not obvious to me what they should do — is there a reason why -(::Bool) or -1 * false need to be defined at all?

1 Like

I agree that -false does not need to be a strong zero. I am more curious about the best practice when using or writing code. :smiley: If I write something similar to rotate!, how should I choose between y = -conj(s)*x + c*y and y = c*y - conj(s)*x, as they are not equivalent given s==false and x==NaN (or Inf)? It could be helpful if there were a convention I could follow.

Another question that just occurred to me :sweat_smile: is the behavior of +false. Julia documentation also describes the unitary plus as the identity operation. But

julia> identity(false)
false

julia> +false
0

-false needs to be an Integer because -true is an integer. Otherwise the function -(x::Bool)wouldn’t be type stable. On the other hand, the unary plus could be defined as +(x::Bool) = x, I don’t think that would cause any problems.

5 Likes

I wonder what the reason is for the method +(x::Bool) = int(x). It was added in https://github.com/JuliaLang/julia/commit/d2f319f1c6a5193fe068c5a1d057f70899d58b5c without much explanation. One would expect x \cdot y = x \cdot (+y) holds for any numbers x, y, but it clearly doesn’t hold with this method in place (NaN * false == 0.0 and NaN * (+false) == NaN * 0 == NaN).

1 Like

Wow that’s 2013, it’s the stone age of Julia. I guess there wasn’t any reason. You could ask @jeff.bezanson, though.

1 Like

I think that the current behavior is consisent, if one considers the rules of operator precedence, and that the ā€œstrong-zeronessā€ of false is not propagated to zeros of other types (even if derived from operating with false). Going through the examples given by the OP and other participants of this thread:

julia> -NaN * false # NaN * strong zero = normal zero promoted to `Float64`
-0.0

julia> -false # additive inverse of strong zero = normal zero of default integer type
0

julia> -1 * false # -1 * strong zero = normal zero promoted to Int, same as before
0

julia> NaN * -false # NaN * (-false = 0) = NaN
NaN

julia> 1 - NaN * false # 1 - (NaN * false = 0.0) = 1.0
1.0

julia> 1 - false * NaN # 1 - (false * NaN = 0.0) = 1.0
1.0

julia> - false * NaN + 1 # ((-false = 0.0) * NaN = NaN ) + 1 = NaN
NaN

julia> - NaN * false + 1 # (-NaN * false = 0.0) + 1 = 1.0
1.0
2 Likes

Somehow thought a strong zero meant result for multiplication should also be strong zero, not sure where I got that from… :sweat_smile:

Every other non-Boolean operation also promotes Bool to Int, e.g.

julia> true - true # integer 0, not false
0

So I’d say that it is for the sake of consistency - although that is an exception to the definition of unary plus as identity of operator for primitive types. Should that exception be explicitly mentioned?

Yes, as best as I can remember that is the reason: all arithmetic operations promote Bool to Int. So +x isn’t the identity function exactly, but I’m not sure what to call it. Arithmetic identity? Identity except for Bool :joy: ? I wonder if there are other types in the ecosystem like Bool in this way?

Looking through our methods, I would say this is wrong:

-(A::AbstractArray) = broadcast_preserving_zero_d(-, A)

+(x::AbstractArray{<:Number}) = x

the second method should look like the first IMO.

6 Likes

I like the current way of achieving consistency in Julia @heliosdrm, @jeff.bezanson, just wondering if I should now always write a - b * c rather than -b * c + a (or the other way around) to be consistent in my code. :sweat_smile: And if it is too much to expect other packages to pick a convention (at least within that package). :thinking:

You should write fma(-b, c, a) :wink:
Yeah, this is a tradeoff in allowing false to be a ā€œstrong zeroā€. NaN*im works, but some identities no longer hold. I would probably prefer forms with fewer operations. Other than that just don’t worry about NaNs too much :slight_smile:

2 Likes

Thanks!

And just for the sake of reference…:joy:

julia> fma(false, NaN, 1)
NaN

julia> muladd(false, NaN, 1)
NaN

julia> false * NaN + 1
1.0

Yeah it might make sense to add methods for those. Some might argue that you only use fma if you know you have floats, but it can be debated in an issue or PR.

1 Like

In case this detail is deemed worth to be explained in the manual:

1 Like

I’ve just created a tangentially related issue about unary + for arrays with Bool values:

3 Likes