4
votes

I'm re-reading The Julia Language Documentation in interest for building a performance sound queue application, and the section regarding parametric constructors and outer constructors has me confused. The example they show is as follows:

julia> struct OurRational{T<:Integer} <: Real
           num::T
           den::T
           function OurRational{T}(num::T, den::T) where T<:Integer
               if num == 0 && den == 0
                    error("invalid rational: 0//0")
               end
               g = gcd(den, num)
               num = div(num, g)
               den = div(den, g)
               new(num, den)
           end
       end

julia> OurRational(n::T, d::T) where {T<:Integer} = OurRational{T}(n,d)
OurRational

julia> OurRational(n::Integer, d::Integer) = OurRational(promote(n,d)...)
OurRational

julia> OurRational(n::Integer) = OurRational(n,one(n))
OurRational

The documentation states:

The first is the "standard" general (outer) constructor that infers the type parameter T from the type of the numerator and denominator when they have the same type.

From this I come to understand this constructor handles the case of two parameters. It infers T from (num and den)'s type, then supplies it to the OurRational internal constructor.

The documentation goes on to say:

The second applies when the given numerator and denominator values have different types: it promotes them to a common type and then delegates construction to the outer constructor for arguments of matching type.

Looking at the code, I do not see how this is case. Based on how I think Julia is evaluating, wouldn't the second constructor handle the case where both n and d are explicit Integers? If otherwise, I am seriously having trouble seeing how Julia sees this line. Also, the ellipses in the assignment suffix means something like "now go to the outer constructor for when both parameters have the same type" right?

I understand the third outer constructor well.

1
promote(x, y) returns a tuple, and the ellipses just unpack the tuple so the function sees the arguments as separate and not a single tuple argument.Bill

1 Answers

4
votes

The first and second outer constructors differ because the second one handles a case where the numerator and denomenator are both integers, but are two different concrete subtypes, both of the abstract type Integer.

Julia as you know has many builtin Integer types. Consider for example UInt8, which is the default type for single byte syntax, such as 0x1f. Look at the results when we mix UInt8 and the default Integer type, Int64:

julia> struct OurRational{T<:Integer} <: Real
           num::T
           den::T
           function OurRational{T}(num::T, den::T) where T<:Integer
               println("Inner")
               if num == 0 && den == 0
                   error("invalid rational: 0//0")
               end
               g = gcd(den, num)
               num = div(num, g)
               den = div(den, g)
               new(num, den)
           end
       end

julia>

julia> OurRational(n::T, d::T) where {T<:Integer} = begin println("Outer 1"); OurRational{T}(n,d) end
OurRational

julia> OurRational(n::Integer, d::Integer) = begin println("Outer 2"); OurRational(promote(n,d)...) end
OurRational

julia> OurRational(n::Integer) = begin println("Outer 3"); OurRational(n,one(n)) end
OurRational

julia> OurRational{UInt8}(0x2, 0x5)
Inner
OurRational{UInt8}(0x02, 0x05)

julia> OurRational(2, 5)
Outer 1
Inner
OurRational{Int64}(2, 5)

julia> OurRational(0x2, 5)
Outer 2
Outer 1
Inner
OurRational{Int64}(2, 5)

julia> OurRational(0x3)
Outer 3
Outer 1
Inner
OurRational{UInt8}(0x03, 0x01)

So, the second outer constructor promotes the arguments to same type and then hands them off to the first outer constructor, which then hands off to the inner constructor.