# 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â€¦

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.