julia> struct A{T}
a::T
b::Vector{Int}
end
julia> A(1, [1])
A{Int64}(1, [1])
julia> A(1, [1,2])
A{Int64}(1, [1, 2])
julia> A(1, 1:2)
ERROR: MethodError: no method matching A(::Int64, ::UnitRange{Int64})
Closest candidates are:
A(::T, ::Array{Int64,1}) where T at REPL[1]:2
Stacktrace:
[1] top-level scope at none:0
julia> A{Int}(1, 1:2)
A{Int64}(1, [1, 2])
julia> A(a::T, b) where {T} = A{T}(a, b)
A
julia> A(1, 1:2)
A{Int64}(1, [1, 2])
The problem is that the default constructor A(a,b) does not perform any automatic conversions although the constructor A{Int}(a::Int,b) does perform an automatic conversion of b to an appropriate type. Also if the struct is not parametric then also automatic conversion is performed.
I find it a bit inconsistent. Was it considered to allow for automatic conversion of non-parametric fields in parametric type? Or maybe there are some problems if this were allowed?
Julia knows this conversion. The problem is that the method signature generated for a default constructor is too specific, see here:
julia> struct A{T}
a::T
b::Vector{Int}
end
julia> methods(A)
# 1 method for generic function "(::Type)":
[1] (::Type{A})(a::T, b::Array{Int64,1}) where T in Main at REPL[1]:2
julia> methods(A{Int})
# 1 method for generic function "(::Type)":
[1] (::Type{A{T}})(a, b) where T in Main at REPL[1]:2
and you see that the signature for b in the first case (constructor without a parameter) is restricted, but it is not restricted for a constructor with a parameter.
I think it is actually consistent with method dispatch. The default constructor defines a specific method, but automatic conversion would mess up with type dispatch. The default constructor is just defining a method just like any other function.
Yeah, I agree with you here. The key thing is that without any type parameters, we get two methods:
julia> struct B
a::Int
b::Float64
end
julia> methods(B)
# 2 methods for generic function "(::Type)":
[1] B(a::Int64, b::Float64) in Main at REPL[4]:2
[2] B(a, b) in Main at REPL[4]:2
The first is the actual constructor, the second is converts arguments appropriately.
With a type parameter, we don’t get the converting method defined on A — it’s only defined on A{T}.
julia> struct A{T}
a::T
b::Int
c::Float64
end
julia> methods(A)
# 1 method for generic function "(::Type)":
[1] (::Type{A})(a::T, b::Int64, c::Float64) where T in Main at REPL[1]:2
This is my point. In general it is possible that in complex signatures it might be non-trivial/impossible to derive type parameters in a “converting” constructor unambiguously, but already the rule:
if a field type is parameterized in any way (i.e. depends on a type parameter) then it is not converted but its type passed exactly as-is to the constructor method signature;
if a field type does not depend on any parameter of the type then it is converted;
would improve things and probably would cover most use-cases in practice.