Zero(π) and one(π) are false and true?

The code for irrationals.jl in base defines:

zero(::AbstractIrrational) = false
zero(::Type{<:AbstractIrrational}) = false

one(::AbstractIrrational) = true
one(::Type{<:AbstractIrrational}) = true

So, then, for example:

julia> zero(π)
false

julia> one(π)
true

This was unexpected.

What is the reasoning behind this choice?

3 Likes

AbstractIrrational <: Real, so zero and one should return some kind of real number with the numerical value 0 and 1.

The docs of false states:

Moreover, false acts as a
  multiplicative "strong zero":

  julia> false == 0
  true

  julia> true == 1
  true

  julia> 0 * NaN
  NaN

  julia> false * NaN
  0.0

It’s likely that is the motivation for returning Bool values.

6 Likes

I think that this result is not correct. According to the docs, zero(T::Type) and one(T::Type) return the additive and multiplicative identities for T, respectively. But in this case that’s not true:

julia> zero(π) + π == π
false

julia> one(π) * π == π
false
6 Likes

That is expected - julia is not a CAS and trying to preserve the Irrational types in particular quickly leads to trying to make julia one.

The particular choice here is also intentional, see @edit ==((zero(π) + π), π):

# Irrationals, by definition, can't have a finite representation
# equal them exactly
==(x::AbstractIrrational, y::Real) = false
==(x::Real, y::AbstractIrrational) = false

since mathematical operations on irrationals lead to them being “Realized” to the corresponding precision.

12 Likes

To be clear you could add methods like

julia> Base.:+(x::Bool, y::Irrational) = x ? 1 + y : y

julia> false + π === π
true

to make Irrationals behave a little better.

But that’s the problem with Irrational, there’s always something that could be improved a little bit. Maybe you want a specialized method to make sin(π) return 0 instead of 1.2246467991473532e-16? That’s easy to add as well. What about sin(2π), maybe not as easy but doable with some more type tricks. Continuing along this road leads you to a full blown Computer Algebra System, which would be completely unrealistic to build into Julia Base.

7 Likes

Actually:

julia> zero(π) + π ≈ π
true

julia> 0.0 + π == π
false

julia> one(π) * π ≈ π
true

That’s interesting, is that a warning, or is this wanted?

Is that from some (older?) Julia version? Julia 1.8.1 (for me, and should for all, I believe no portability issue) DOES return 0.0 exactly (I think they tried reall hard for that to happen unlike for other multiples of π). But not for bitstring(sin(2π)), nor even sin(-π), does for sin(0) though. There’s a reason for sinpi.

3 Likes

But if

julia> zero(π) + π == π
false

then there’s an argument that either this should be fixed, or methods for one and zero should be removed.

There may be a blurry line somewhere between what should be included and what shouldn’t, but I don’t think that line should lie between the existence of zero(::AbstractIrrational) and the correct behavior of zero(π) + π.

3 Likes

I found this related issue, in case this conversation leads to something actionable:

1 Like

That’s very much intentional, see the bottom of section Mathematical Operations and Elementary Functions · The Julia Language.

Yes, I was still on 1.7. Apparently it was decided to implement exactly that for 1.8:

julia> @which sin(π)
sin(::Irrational{:π}) in Base.MathConstants at mathconstants.jl:126
1 Like

I don’t have a strong opinion on the line for irrationals, but I suspect that removing zero and one would break a lot of things in the presence of irrationals.

Yes, but I thought the polynominal might have been changed, but apparently not:

julia> sin(Float64(π))
1.2246467991473532e-16

So just making sure for:

Base.sin(::Irrational{:π}) = 0.0
Base.cos(::Irrational{:π}) = -1.0
Base.sincos(::Irrational{:π}) = (0.0, -1.0)
Base.tan(::Irrational{:π}) = 0.0
Base.cot(::Irrational{:π}) = -1/0

unlike for:
julia> cos(Float64(π))  #even exact with: bitstring(cos(Float16(π)))
-1.0

julia> typeof(-π)  # so not possible for it currently (or would there have been some possible workaround to get sin(-π) exact?)
Float64

There’s already:

I think 2π (as a literal constant) could have been changed to a separate “2π” irrational, and made exact, maybe not good under that name, since variables/constants cant start with number, rather twopi, or simply tau. I think they simply (wisely) chose to not do it in Julia, since can be done in a package; it was maybe a mistake to include any irrationals in Julia, this is a can of worms, and Julia itself doesn’t need them.

Unary minus on irrationals could also have been defined on irrationals, to give their mirrors, and then you have at least 4 pi-related irrationals. At some point it will be surprising, e.g. that 3π or simply 2.0π. So you would have to multiply again by 2 (at least), the number of irrational types, to support float (well Float64 only).

Naturally, removing one and zero is not an option. But handling *(::Bool, ::AbstractIrrational), etc., seems like a decent possibility, despite the ‘slippery slope’ argument (I actually think the slippery part lies between those two options). Unless, of course, that too would break stuff.

Thanks for the Knuth reference (just adds one more obscure trivia you need to know for [IEEE] floating point; not sure it applies to Post floating point, and its NaR).

Since false as “strong zero” is meant to disable propagation of NaN, when intended, explicitly used, is it then a bug related to irrationals (false and true, had other reasons for them there, related to Float64 or no other type good enough), to silently disable NaN propagation (which is a good thing, except when you want to turn it off)?

julia> faraway_calculation_results_in = NaN

julia> faraway_calculation_results_in*zero(π)  # not sure how often this, or similar, would be done in practice though.
0.0

unlike:
julia> faraway_calculation_results_in*zero(Float64(π))
NaN

[File a (separate) bug? Or is it simply a known issue, part of the other open bug linked from this thread?]

This might have to do with it (didn’t look into it carefully):

Isn’t one(π) * π != π a bona fide bug, then?

help?> one
 Return a multiplicative identity for x: a value such that one(x)*x == x*one(x) == x.

It’s not just a question of improving numerical precision, the definitions of one and zero are that they should be identities.

1 Like

Couldn’t there be an Irrational{true/false} representing this?

Some more methods would be needed to make it work properly, but something like this could be one way to handle zero/one identities?

julia> Base.zero(::AbstractIrrational) = Irrational{false}()

julia> Base.zero(::Type{<:AbstractIrrational}) = Irrational{false}()

julia> Base.one(::AbstractIrrational) = Irrational{true}()

julia> Base.one(::Type{<:AbstractIrrational}) = Irrational{true}()

julia> Base.:+(a::Irrational{false}, b::AbstractIrrational) = b

julia> Base.:+(a::AbstractIrrational, b::Irrational{false}) = b

julia> Base.:*(a::Irrational{true}, b::AbstractIrrational) = b

julia> Base.:*(a::AbstractIrrational, b::Irrational{true}) = b

julia> Base.Float64(::Irrational{false}) = zero(Float64)

julia> Base.Float64(::Irrational{true}) = one(Float64)

julia> zero(π) + π == π
true

julia> one(π) * π == π
true

julia> one(π) + π == π
false

julia> zero(π) * π == π
false
1 Like

Isn’t one(π) * π != π a bona fide bug, then?

No, or this is tricky, where is the bug? one(π) == true (meaning “1”) IS the multiplicative identity of π, just regular math).

The “bug” if you will then happens with true * π no longer being an irrational (also for false). That (and for false) seemingly could be fixed, but not for true + π (while false + π could be made to work).

julia> one(π) * π ≉ π  # EDIT:  !(one(π) * π ≈ π)  # I didn't locate an \notapprox operator, is there one?
false

\napprox

1 Like

Well, I would say that if true isn’t the multiplicative identity of pi, then one(pi) shouldn’t return true. So the bug is in one.

1 Like

I think these issues come down to two basic issues:

  1. Comparing real numbers for equality is undecidable
  2. Implementing a CAS is not in scope for Julia Base

Perhaps Irrational shouldn’t ever have been a type, instead a better design (given the above limitations and expectations of consistency with the rest of Julia) might have been to make each irrational constant a function that takes a <:Real type.
Something like this:

irrational_pi(Float64)  # evaluates to 3.1...

or this:

irrational(:pi, Float64)  # evaluates to 3.1...
3 Likes