Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for non-zero denominator in iszero for Rational #54874

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

jishnub
Copy link
Contributor

@jishnub jishnub commented Jun 21, 2024

Typically, one can't construct 0//0, and it's not a valid Rational, so this issue doesn't show up in standard use cases. However, 0//0 pops up occasionally in arrays constructed with similar, and having this treated as 0 messes with structured matrix detection logic. I am not convinced that this should be iszero, since this isn't a valid number in the first place.

Currently,

julia> C = reinterpret(reshape, Rational{Int}, zeros(Int, 2, 2, 2))
2×2 reinterpret(reshape, Rational{Int64}, ::Array{Int64, 3}) with eltype Rational{Int64}:
 0//0  0//0
 0//0  0//0

julia> all(iszero, C)
true

This PR would change this to

julia> any(iszero, C)
false

@jishnub jishnub added rationals The Rational type and values thereof maths Mathematical functions labels Jun 21, 2024
@oscardssmith
Copy link
Member

How much does this degrade performance?

@mikmoore
Copy link
Contributor

mikmoore commented Jun 21, 2024

I imagine the performance difference is negligible, but it seems strange to go out of the way to handle unconstructable values. reinterpret is known to produce potentially invalid values and so is reading from uninitialized arrays. Operations on malformed inputs are not guaranteed to be correct across most of the places where they occur.

0//0 is invalid. Is it zero? Not exactly. But is it not zero? Hard to say. The issue is that in some cases you check for zero because you want a zero in that spot and in other cases you check for zero because you do not. There is no consistently useful stance one can take on the value of an invalid. So while I understand it was useful to treat invalids as nonzero in your motivating example, I don't think this is a universally useful solution. It will be exactly opposite to some peoples' needs.

Even with this (particular) fix, reinterpret(Rational{Int}, (0, -1)) will be iszero. This rational is invalid and unconstructable just like 0//0, but something went very wrong so it doesn't seem responsible to call it iszero either. Should reinterpret(Rational{Int}, (3,3)) be isone? These functions should at least be consistent in their treatment of invalids.

It would seem that this PR starts the slippery slope toward checking isvalid at every call site with Rational (but it actually doesn't even properly check for validity, as written). And further for every type that possesses invalid bitstrings. That seems ridiculous (all those checks would add up in cost), so I'm reluctant to start it here.


I would prefer that this PR simply delete this method (and the matching isone) entirely and just use the fallback iszero(x::Any) = x == zero(x). The only drawback of this change is that it would allocate for BigInt, so I would also accept iszero(x::Rational) = iszero(numerator(x)) & isone(denominator(x)) (equivalent to the fallback but does not construct a new value, and it also mirrors the current implementation of isone). Either version would ensure consistent behavior on invalids while also satisfying your motivating issue.

I don't love testing for specific valid-like behavior on invalid values. That would seem to imply this as API that becomes at least a minor change to adjust later. EDIT: I do think that all invalids should not satisfy iszero, so the test is useful but should check more than just 0//0.

@nsajko
Copy link
Contributor

nsajko commented Jun 21, 2024

Viewpoint to consider: 0 // 0 is special and a lot like NaN. Maybe Julia should even have isnan(Base.unsafe_rational(0, 0))?

@longemen3000
Copy link
Contributor

Viewpoint to consider: 0 // 0 is special and a lot like NaN. Maybe Julia should even have isnan(Base.unsafe_rational(0, 0))?

Yes! It follows a lot of its properties

@mikmoore
Copy link
Contributor

mikmoore commented Jun 21, 2024

I think discussion of 0//0 as NaN belongs on a separate issue. It has negligible impact on the issue here.

@jishnub
Copy link
Contributor Author

jishnub commented Jun 21, 2024

But is it not zero? Hard to say. The issue is that in some cases you check for zero because you want a zero in that spot and in other cases you check for zero because you do not. There is no consistently useful stance one can take on the value of an invalid

I'm intrigued by this: is there a motivating example where one does want to treat 0//0 as 0? This seems to counter the properties that one may expect from such a number.

@mikmoore
Copy link
Contributor

mikmoore commented Jun 21, 2024

I'm intrigued by this: is there a motivating example where one does want to treat 0//0 as 0? This seems to counter the properties that one may expect from such a number.

What I attempted and failed to allude to is the difference between the following two lines, either of which a sane person could write (if sqrt didn't do this already)

x >= 0 ? sqrt(x) : error() # look for what I want
x < 0 ? error() : sqrt(x) # reject what I don't want

These two appear somewhat equivalent at first glance, but they are not equivalent. The only difference here (for most Real, at least) is the handling of NaN, where the first will error but the second will pass the NaN to the calculation (Julia's sqrt uses the second pattern because we choose to error on negatives but not on NaN). Someone implementing the same calculation where you encountered your problem, but from the opposite paradigm, might find it more convenient that we do group 0//0 as iszero. Although I'm not actually advocating we do that, either.

For example, a responsible coder might assume that !iszero(x) implies !iszero(numerator(x)). This is true for all valid Rational and, prior to this PR, also all invalids. But I'm not proposing we keep that as it isn't the "sanest" invariant to preserve.

When the invariants of the type are violated, it becomes very situational what the "correct" fallback behavior should be. I find the "look for what I want" pattern to be slightly more robust to this, which is why I prefer it. But there are no answers that are "best" or "most convenient" in all situations.

In any case, I will still advocate for one of my counter-proposals that fulfill the invariant that iszero(x) imply x == zero(x) (i.e., iszero is always false for invalids). Anything else is technically wrong (by @doc iszero), although experiencing incorrect behavior on invalids isn't objectionable (hence my puzzlement over this PR in the first place).

@jishnub
Copy link
Contributor Author

jishnub commented Jun 22, 2024

Alright, thanks, I'm starting to get convinced that this might not be the right approach

@jishnub jishnub closed this Jun 22, 2024
@mikmoore
Copy link
Contributor

I still think iszero(x::Rational) = iszero(numerator(x)) & isone(denominator(x)) would be a reasonable improvement over the status quo and would match the isone implementation. My concerns was mostly over being too opinionated on invalids. But such a version would reject all invalids as not iszero.

@jishnub jishnub reopened this Jun 24, 2024
@jishnub
Copy link
Contributor Author

jishnub commented Jun 24, 2024

Should the test be updated as well? Although, I can't think of cases other than invalids where the denominator check would be necessary.

@jishnub jishnub changed the title Don't count 0//0 as zero Check for non-zero denominator in iszero for Rational Jun 24, 2024
@mikmoore
Copy link
Contributor

Maybe just check for a few "prototypical" ones?

iszero(zero(Rational{Int})) == true
iszero(reinterpret(Rational{Int}, (0, typemin(Int)))) == false
iszero(reinterpret(Rational{Int}, (0, -1))) == false
iszero(reinterpret(Rational{Int}, (0, 0))) == false
iszero(reinterpret(Rational{Int}, (0, 2))) == false
iszero(reinterpret(Rational{Int}, (0, typemax(Int)))) == false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maths Mathematical functions rationals The Rational type and values thereof
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants