Search code examples
syntaxjuliadocumentation

Why does this line from "The Julia Language Documentation" define an outside constructor for a case where input parameteres are different types?


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.


Solution

  • 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.