Undefined constructor is executed


#1

Consider the following type:

julia> VERSION
v"0.6.1-pre.0"

julia> struct MyType{T}
           x::Float64
           y::T
       end

According to this documentation, such type definition implicitly defines an outer constructor MyType(x::Float64, y::T) where {T} = MyType{T}(x,y). Therefore, we can construct a MyType instance as

julia> MyType(1.0, 2.0)
MyType{Float64}(1.0, 2.0)

but passing a non-Float64 first argument or missing the second argument generates an error:

julia> MyType(1, 2.0)
ERROR: MethodError: no method matching MyType(::Int64, ::Float64)
Closest candidates are:
  MyType(::Float64, ::T) where T at REPL[1]:2

julia> MyType(1.0)
ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type MyType
This may have arisen from a call to the constructor MyType(...),
since type constructors fall back to convert methods.
Stacktrace:
 [1] MyType(::Float64) at ./sysimg.jl:24

Now, let’s define an additional outer constructor that can handle both the above error-generating cases:

julia> MyType(x::Real, y::T=nothing) where {T} = (println("Executed"); MyType{T}(x,y))
MyType

Note that "Executed" must be printed out every time this constructor is executed. If we repeat the above two constructions using this newly defined outer constructor, we get

julia> MyType(1, 2.0)
Executed
MyType{Float64}(1.0, 2.0)

julia> MyType(1.0)
MyType{Void}(1.0, nothing)

Strangely, the second construction does not print out "Executed"! A constructor other than the above defined outer constructor seemed to be used here.

A further test:

julia> MyType(1)
Executed
MyType{Void}(1.0, nothing)

This time, "Executed" is correctly printed out. So, it seems that the defined outer constructor is used when automatic type conversion from Int64 to Float64 for x is required, whereas the constructor is not used if a Float64 x is given.

Why is this happening? How can we explain this phenomenon?


#3

MyType(1.0) translates to MyType(1.0, nothing) and implicit outer constructor is called as Float64 is more specific than Real.
The explanation how optional arguments are handled is given here https://docs.julialang.org/en/stable/manual/methods/#Note-on-Optional-and-keyword-Arguments-1.


#4

And MyType(1) translates to MyType(1, nothing) and your additional constructor gets called as Int is not Float64.
Observe that inner constructor MyType{T}(x,y) does not have restrictions on types of arguments and it allows passing Int as x that gets converted to Float64 like it would get converted in assignment x::Float64 = 1.


#5

OK. I think the following sentence in the documentation you linked is important: “In other words, optional arguments are tied to a function, not to any specific method of that function.” According to that sentence, is this correct explanation?

  • When MyType(1.0) is called, the optional second argument nothing is appended due to the explicit outer constructor MyType(x::Real, y::T=nothing) where {T} = ....
  • This results in calling MyType(1.0, nothing). However, the fact that the optional argument is appended due to one constructor does not mean that the body of that constructor is executed, because “optional arguments are tied to a function, not any specific method of the function.”
  • Therefore, in this case calling MyType(1.0) is equivalent to calling MyType(1.0, nothing) separately, which of course invokes the implicit outer constructor that is more specific than the explicit outer constructor.

#6

The shorter (equivalent to yours) explanation is that definition:

MyType(x::Real, y::T=nothing) where {T} = (println("Executed"); MyType{T}(x,y))

translates to:

MyType(x::Real, y::T) where {T} = (println("Executed"); MyType{T}(x,y))
MyType(x::Real) = MyType(x, nothing)

and the second definition simply calls the most specific MyType. You can see this by running @code_lowered:

julia> @code_lowered MyType(1)
CodeInfo(:(begin
        nothing
        return (#self#)(x, Main.nothing)
    end))

julia> @code_lowered MyType(1.0)
CodeInfo(:(begin
        nothing
        return (#self#)(x, Main.nothing)
    end))

julia> @code_lowered MyType(1, nothing)
CodeInfo(:(begin
        nothing
        (Main.println)("Executed")
        return ((Core.apply_type)(Main.MyType, $(Expr(:static_parameter, 1))))(x, y)
    end))

julia> @code_lowered MyType(1.0, nothing)
CodeInfo(:(begin
        nothing
        return ((Core.apply_type)(Main.MyType, $(Expr(:static_parameter, 1))))(x, y)
    end))