Surprise with Rational

I dont understand why 0 * 1//0 blows up

julia> 1//0 * 2
1//0

julia> 1//0 * 1
1//0
julia> 1//0 * 0
ERROR: DivideError: integer division error
Stacktrace:
 [1] div
   @ ./int.jl:288 [inlined]
 [2] divgcd(x::Int64, y::Int64)
   @ Base ./rational.jl:45
 [3] *(x::Rational{Int64}, y::Int64)
   @ Base ./rational.jl:339
 [4] top-level scope
   @ REPL[63]:1
julia> 1//0 * zero(Rational)
ERROR: DivideError: integer division error
Stacktrace:
 [1] div
   @ ./int.jl:288 [inlined]
 [2] divgcd(x::Int64, y::Int64)
   @ Base ./rational.jl:45
 [3] *(x::Rational{Int64}, y::Rational{Int64})
   @ Base ./rational.jl:335
 [4] top-level scope
   @ REPL[64]:1
julia> 1//0 * 0//3
ERROR: DivideError: integer division error
Stacktrace:
 [1] div
   @ ./int.jl:288 [inlined]
 [2] divgcd(x::Int64, y::Int64)
   @ Base ./rational.jl:45
 [3] *(x::Rational{Int64}, y::Rational{Int64})
   @ Base ./rational.jl:335
 [4] top-level scope
   @ REPL[65]:1

What part do you find surprsing?

0/0 is not well defined. It’s analogous to what IEEE has:

julia> 0.0 / 0.0
NaN

julia> 1.0 / 0.0
Inf

except here we don’t have NaN because you’re not dealing with IEEE floating point numbers, so blow up is pretty reasonable

2 Likes

My example is using *

so?

for rational numbers, (a//b) * c is exactly the same as (a*c) // (b), in the end you have 0//0 no?

I mean observe:

julia> float(1//0)
Inf

julia> float(2//0)
Inf
3 Likes

(a//b) * c being same as (a*c) // b and 0//0 being an error is a useful way to think about it, thanks

(its confusing that its a different error though)

julia> 0//0
ERROR: ArgumentError: invalid rational: zero(Int64)//zero(Int64)
Stacktrace:
 [1] __throw_rational_argerror_zero(T::Type)
   @ Base ./rational.jl:32
 [2] Rational
   @ ./rational.jl:34 [inlined]
 [3] Rational
   @ ./rational.jl:39 [inlined]
 [4] //(n::Int64, d::Int64)
   @ Base ./rational.jl:62
 [5] top-level scope
   @ REPL[67]:1
1 Like

that’s thrown by constructor, you’re doing arithmetic, where an intermediate step throws error already, I mean, I don’t think they are confusing since they are pretty much saying the same thing: you can’t \div by integer 0

julia> 1Ă·0
ERROR: DivideError: integer division error
Stacktrace:
 [1] div(x::Int64, y::Int64)
   @ Base ./int.jl:288
 [2] top-level scope
   @ REPL[2]:1

but yeah, maybe we can improve this somehow

When is it rational (pun intended) to define 1//0 (or even better 2//0)? Should it be disallowed in the constructor (it would be slightly faster, but breaking, not in a major way?). It’s similar to defining Inf (or -Inf), and I’m not sure how often you do that on purpose (for floats, or at least rationals). Julia could have a different way of defining Inf_rat, and also a show methods that shows Inf_rat, not 1//0 or 2//0 etc.

The error message seems by design, but could be amended, and Julia as fast:

@noinline __throw_rational_argerror_zero(T) = throw(ArgumentError("invalid rational: zero($T)//zero($T)"))
function Rational{T}(num::Integer, den::Integer) where T<:Integer
    iszero(den) && iszero(num) && __throw_rational_argerror_zero(T)
    num, den = divgcd(num, den)
    return checked_den(T, T(num), T(den))
end

Whatever it is, it’s breaking change

It was intentionally allowed in order to represent Inf with a Rational:

3 Likes

By the same spirit, why wouldn’t 0//0 be allowed in order to represent NaN with a Rational?

The inconsistency is noteworthy and causes some hilariously bad behavior. Take some code with Float64:

julia> harmonicmean(a, b) = 2/(1/a + 1/b);

julia> harmonicmean(1, 1/0)       # note 1/0 == Inf
2.0

julia> harmonicmean(1, (1+1im)/0) # note (1+1im)/0 == Inf + Inf*im
2.0  + 0.0im

julia> harmonicmean(1, (1+0im)/0) # note (1+0im)/0 == Inf + NaN *im
2.0  - 0.0im

julia> harmonicmean(1, 1im/0)     # note 1im/0 == NaN  + Inf*im
2.0  + 0.0im

All this behavior is expected; just dealing with different versions of infinity strewn about the complex plane.

Compare with Rational:

julia> harmonicmean(a::Rational, b::Rational) = 2//(1//a + 1//b);

julia> harmonicmean(1//1, 1//0)
2//1

julia> harmonicmean(1//1, (1+1im)//0)
2//1 + 0//1*im

julia> harmonicmean(1//1, (1+0im)//0)
ERROR: ArgumentError: invalid rational: zero(Int64)//zero(Int64)
Stacktrace:
 [1] __throw_rational_argerror_zero(T::Type)
   @ Base .\rational.jl:32
 [2] Rational
   @ .\rational.jl:34 [inlined]
 [3] Rational
   @ .\rational.jl:39 [inlined]
 [4] //
   @ .\rational.jl:62 [inlined]
 [5] //(x::Complex{Int64}, y::Int64)
   @ Base .\rational.jl:78
 [6] top-level scope
   @ REPL[42]:1

julia> harmonicmean(1//1, 1im//0)
ERROR: ArgumentError: invalid rational: zero(Int64)//zero(Int64)
Stacktrace:
 [1] __throw_rational_argerror_zero(T::Type)
   @ Base .\rational.jl:32
 [2] Rational
   @ .\rational.jl:34 [inlined]
 [3] Rational
   @ .\rational.jl:39 [inlined]
 [4] //
   @ .\rational.jl:62 [inlined]
 [5] //(x::Complex{Int64}, y::Int64)
   @ Base .\rational.jl:78
 [6] top-level scope
   @ REPL[69]:1

Sure the error is thrown before the call to harmonicmean, but that’s not the point.

1 Like

Hm… :thinking:

julia> (1/0 + 0im)
Inf + 0.0im

julia> (1//0 + 0im)
1//0 + 0//1*im

julia> (1/0 + 0im)^-1
0.0 - 0.0im

julia> (1//0 + 0im)^-1
0//1 + 0//1*im

julia> 1/(1/0 + 0im)
0.0 - 0.0im

julia> 1/(1//0 + 0im)
0//1 + 0//1*im

julia> 1//(1//0 + 0im)
ERROR: DivideError: integer division error
Stacktrace:
 [1] div
   @ .\int.jl:288 [inlined]
 [2] divgcd(x::Int64, y::Int64)
   @ Base .\rational.jl:45
 [3] //
   @ .\rational.jl:74 [inlined]
 [4] //(x::Complex{Rational{Int64}}, y::Rational{Int64})
   @ Base .\rational.jl:78
 [5] //(x::Int64, y::Complex{Rational{Int64}})
   @ Base .\rational.jl:79
 [6] top-level scope
   @ REPL[420]:1

ehhh because

In computing, NaN standing for Not a Number,

NaN is more or less like a sentinel value to say “hey it’s undefined” without aborting the program, I personally like to imagine if IEEE floating points were designed in an age where error handling is more systematic, it might have been an error.

but that’s exactly the point? does this bother you?

julia> f(x) = x
f (generic function with 1 method)

julia> f(0/0)
NaN

julia> f(0Ă·0)
ERROR: DivideError: integer division error
Stacktrace:
 [1] div(x::Int64, y::Int64)
   @ Base ./int.jl:288
 [2] top-level scope
   @ REPL[3]:1

Hm, never has. I like when languages do the smartest thing possible, but when using the Ă· operator, I do so accepting that integer arithmetic is more limited.

Let me turn the question around on you. Would you propose that all the following six lines should throw errors?

julia> 1/0
Inf

julia> 0/0
NaN

julia> 1Ă·0
ERROR: DivideError: integer division error

julia> 0Ă·0
ERROR: DivideError: integer division error

julia> 1//0
1//0

julia> 0//0
ERROR: ArgumentError: invalid rational: zero(Int64)//zero(Int64)

[Triva] Yes, I thought so, but unlike in IEEE Inf (only two Inf and -Inf, or for each precision), curously rationals actually have infinitly many representations (well finite for Int64-based, infinite for BigInt-based, or limited by memory). This doesn’t pose any problems:

julia> 1//0 == 2//0  # both +Inf up to Inf//0
true

On the other hand while NaN has many representations in IEEE, it has none in rationals, not even analogous to (I guess not done for rationals for performance reasons):

julia> 0/0
NaN

On top of the many NaN representations in IEEE, it’s in addition signed, by default it’s -NaN and showing as NaN always. I guess -NaN was chosen for sorting reasons, +NaN is:

julia> bitstring(reinterpret(Float64, reinterpret(Int64, NaN) & (2^63-1)))
"0111111111111000000000000000000000000000000000000000000000000000"

it’s not a number… folks, NaN is an “error,” or “exception”, it’s a value because this is the way to handle runtime exception at low level (you can’t throw and catch error at assembly can you).

why would a “error” have a place in our high-level, language-specific rational number system?

I’m not saying rationals need NaN, I’m very ok with an exception thrown on 0//0. [I would also think it might be ok for 0/0]. To me Inf, and -Inf are also “error conditions”, and seemingly could throw (for rationals, likely would be slower; also for floats). It’s intentional to allow converting Inf and -Inf to rationals, but I at least don’t know it’s very useful? When do you construct Inf intentionally (in floats or rationals)?

1 Like

this could happen

julia> rationalize(Inf)
1//0

julia> rationalize(NaN)
ERROR: InexactError: Int64(NaN)
Stacktrace:
1 Like

Personally, I don’t hate 0//0 having NaN-like properties. I consider NaNs to be useful. They allow you complete a whole block of computations before error checking, rather than requiring you to preemptively handle each operation.

There are (were?) processor modes where NaNs trigger traps, rather than quietly propagate. It’s just that they are virtually unused because NaN propagation works so well and traps are a pain.

Rational provides only a single infinity for each sign, just like IEEE754 floats.

julia> 2//0
1//0

julia> 1//0 === 2//0
true
4 Likes

Right, if we chose so, we could have defined it to throw an exception.

I see now one way to generate Infs in rational calculations, i.e. you divide a non-zero by a zero. We need Inf for rationals in one case. It’s not an argument that checking for zero is going to be slower, since there’s already this (slower) check iszero(den) && iszero(num) && __throw_rational_argerror_zero(T) (there’s also Base.checked_den, I might be wrong).

Infs are never generated for other calculations:

julia> 9223372036854775807//1 + 1
ERROR: OverflowError: 9223372036854775807 + 1 overflowed for type Int64

julia> BigInt(9223372036854775807)//1 + 1
9223372036854775808//1

It seems like we could have thrown, not propagate Infs.