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!

1 Like

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
4 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

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)

Sorry to revive this thread, but is there any downside to overriding Base.convert in this way? These particular definitions seem benign, but e.g. this Github issue seems to suggest that overriding Base.convert is a bad idea (β€œtype piracy”).

If overriding Base.convert is a bad idea, is there another way to go about this?

A related question is about the code suggested as working by OP:

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

is this considered type piracy, since it modifies the behavior of VecOrMat?