Union type in struct: type conversion

As a minimum example, one field in my struct can be a complex-valued matrix or vector:

struct foo_1
    a::VecOrMat{ComplexF64}
end

VecOrMat{ComplexF64}(x::Vector) = Vector{ComplexF64}(x)
VecOrMat{ComplexF64}(x::Matrix) = Matrix{ComplexF64}(x)

which allows the input to be, e.g. an integer vector, and convert it to complex floating vector:

foo_1([1, 2])

foo_1(Complex{Float64}[1.0 + 0.0im, 2.0 + 0.0im])

This works as expected; however, another field in my struct can be a floating-point number or vector of such:

struct foo_2
    x::Union{Float64, Vector{Float64}}
end

Union{Float64, Vector{Float64}}(x::Number) = Float64(x)
Union{Float64, Vector{Float64}}(x::Vector) = Vector{Float64}(x)

This however errors:

foo_2(1)

MethodError: Cannot convert an object of type
Int64 to an object of type
Union{Float64, Array{Float64,1}}
Closest candidates are:
convert(::Type{T}, !Matched::T) where T at essentials.jl:171
Union{Float64, Array{Float64,1}}(::Number) at In[3]:5

Stacktrace:
[1] foo_2(::Int64) at ./In[3]:2
[2] top-level scope at In[6]:1

given VecOrMat is just Union{Array{T,1}, Array{T,2}} where T, why is be behavior of foo_2 different from foo_1? @code_typed shows

@code_typed foo_1([1, 2])
CodeInfo(
1 ─ %1 = Main.foo_1::Core.Compiler.Const(foo_1, false)
β”‚   %2 = Core.fieldtype(%1, 1)::Type{Union{Array{Complex{Float64},1}, Array{Complex{Float64},2}}}
β”‚   %3 = invoke Base.convert(%2::Type{Union{Array{Complex{Float64},1}, Array{Complex{Float64},2}}}, _2::Array{Int64,1})::Any
β”‚   %4 = %new(%1, %3)::foo_1
└──      return %4
) => foo_1

and

@code_typed foo_2([1, 2])
CodeInfo(
1 ─ %1 = Main.foo_2::Core.Compiler.Const(foo_2, false)
β”‚   %2 = Core.fieldtype(%1, 1)::Type{Union{Float64, Array{Float64,1}}}
β”‚        Base.convert(%2, x)::Union{}
└──      $(Expr(:unreachable))::Union{}
) => Union{}

I could overload Base.convert() to make foo_2 work,

import Base.convert
convert(::Type{Union{Float64, Vector{Float64}}}, x::Number) = Float64(x)
convert(::Type{Union{Float64, Vector{Float64}}}, x::Vector) = Vector{Float64}(x)

but still wondering the reason why the error happens, and what’s a better way to deal with this error? Thanks!

You should make a parametric type.

(And the convention is to have type names that start with a capital letter.)

struct Foo1{T <: VecOrMat{ComplexF64}}
    a::T
end
3 Likes

@dpsanders recommendation is a good one if you want to specialize based on which type is in the field. There are a few cases where you don’t want to do that, so if you want to continue with your design then @code_typed gives you a clue how to fix it:

julia> @code_typed foo_1([1, 2])
CodeInfo(
1 ─ %1 = Main.foo_1::Core.Compiler.Const(foo_1, false)
β”‚   %2 = invoke Union{Array{Complex{Float64},1}, Array{Complex{Float64},2}}(_2::Array{Int64,1})::Any
β”‚   %3 = %new(%1, %2)::foo_1
└──      return %3
) => foo_1

You can see that the invoke is calling the methods you defined.

In contrast,

julia> @code_typed foo_2(1)
CodeInfo(
1 ─ %1 = Main.foo_2::Core.Compiler.Const(foo_2, false)
β”‚   %2 = Core.fieldtype(%1, 1)::Type{Union{Float64, Array{Float64,1}}}
β”‚        Base.convert(%2, x)::Union{}
└──      unreachable
) => Union{}

calls Base.convert. So you should define the convert method in addition to/instead of the ones you defined:

julia> Base.convert(::Type{Union{Float64, Vector{Float64}}}, x::Number) = Float64(x)

julia> Base.convert(::Type{Union{Float64, Vector{Float64}}}, x::Vector) = Vector{Float64}(x)

julia> foo_2(1)
foo_2(1.0)

To be honest I’m not sure why these two differ.

3 Likes