No automatic type promotion for parametric structs

When using different (but convertible) types in a constructor, for “standard” structs the types get converted automatically, e.g.

struct Struct1
    x:: Float64
    y
end

s1b = Struct1(5, 1) # output: Struct1b(5.0, 1)

However, this is not done if the struct has a type parameter (even if this parameter is for a different field), instead I get a MethodError:

struct Struct2{T}
    x:: Float64
    y:: T
end

s2b = Struct2(5, 1) # MethodError, I would expect Struct2{Int64}(5.0, 1)

s2c = Struct2(5.0, 1) # this works, output: Struct2{Int64}(5.0, 1)

This behavior is confusing for me, I would expect that type promotion is also done for parametric structs. Or is there a good reason for it which I am not aware of?

3 Likes

I think it isn’t about automatic promotion, but rather about
what constructors are automatically provided.

In particular that the constructors provided for a UnionAll (like Struct2 i.e. Struct2{T} where T]) don’t have one that calls convert.

julia> methods(Struct1)  # method of concrete type
# 2 methods for type constructor:
[1] Struct1(x::Float64, y) in Main at REPL[5]:2
[2] Struct1(x, y) in Main at REPL[5]:2

julia> methods(Struct2)  # method of the UnionAll 
# 1 method for type constructor:
[1] Struct2(x::Float64, y::T) where T in Main at REPL[1]:2

julia> methods(Struct2{Int})  # method of concrete type
# 1 method for type constructor:
[1] Struct2{T}(x, y) where T in Main at REPL[1]:2

We can see if we pass the type parameter to Struct2 so we have a concrete type the convert does happen

julia> Struct2{Int}(5, 1.)
Struct2{Int64}(5.0, 1)

So question is kind why isn’t there a constructor
Struct2(a, b::B) where B = Struct2{B}(a, b)

If we define that then it works:

julia> Struct2(a, b::B) where B = Struct2{B}(a, b)
Struct2

julia> Struct2(5,1)
Struct2{Int64}(5.0, 1)
8 Likes

I was just replying the same, but your is way better, so thanks! I still want to know the answer to your question, though:

Why isn’t there an automatic contructor that does this?

5 Likes

I think this could be a good discussion for a new feature, in case it is possible. In case this is intentional and not possible, it might be a good place for an answer.

Previous considerations

There is a discussion for the case where two arguments to the struct are parametric in Types, whether bi-parametric or single-parametric. It says that this constructor is not provided by default because there could be ambiguity between the possible extrapolated types. However, this is not always the case.

Counter example (single parameter)

struct single_param{T}
r::Float64
s::T
end

In this case, the type could be easily extrapolated from the variables passed with a single parametric method.

# Current status
julia> prueba(3,9)
ERROR: MethodError: no method matching single_param(::Int64, ::Int64)
Closest candidates are:
  single_param(::Float64, ::T) where T at REPL[1]:2

# Solution
julia> single_param(r, s::T) where {T} = single_param{T}(r, s)
single_param

julia> single_param(3,9)
single_param{Int64}(3.0, 9)

julia> single_param(3,"4")
single_param{String}(3.0, "4")

Counter example (multi parameter)

struct multi_param{T, U}
q::Float64
r::T
s::U
end

Even in this case, the types could be extrapolated from the variables passed.

# Current status
julia> multi_param(3,9,"7")
ERROR: MethodError: no method matching multi_param(::Int64, ::Int64, ::String)
Closest candidates are:
  multi_param(::Float64, ::T, ::U) where {T, U} at REPL[8]:2

# Solution
julia> multi_param(q, r::T, s::U) where {T, U} = multi_param{T, U}(q, r, s)
multi_param

julia> multi_param(3,9,7im)
multi_param{Int64, Complex{Int64}}(3.0, 9, 0 + 7im)

julia> multi_param(3,9,"t")
multi_param{Int64, String}(3.0, 9, "t")

Conclusions

I take the liberty of summoning @StefanKarpinski in case there is a reason for this (sorry if it is already somewhere, I cannot find it in the docs).

1 Like

Strong recommend moving this to a GitHub issue.
That is where feature requests are discussed for formally.

2 Likes

Thanks for your feedback!
I’ll create an issue for it.

Edit: done - https://github.com/JuliaLang/julia/issues/41241

Thanks! I was wary of doing it because it may already have an explanation and I didn’t want to add more Issues (it is very crowded there).

This is a duplicate of Autogenerate `(::Type{Foo})(x::T, y) where T` constructor for type Foo{T} · Issue #35053 · JuliaLang/julia · GitHub, which has bitten people repeatedly before.

1 Like

Thanks for mentioning it, I have not found it in my searches before.
I found this behavior especially problematic together with Base.@kwdef if (at least) one of the fields is parametric, e.g. if it is a Function (which is an abstract type, making this type parametric brings back type stability).