Conversion of inner "flag" type parameters from supertype "flag" type parameters for composite struct construcor

I have a type structure that looks something like this:

abstract type AbstractA{T} end
struct A1{T} <: AbstractA{T} end # no field of type `T`
A1() = A1{Any}()
struct A2{T} <: AbstractA{T} end
A2() = A2{Any}()

and I would like a conversion mechanism that converts from A1{T} to A1{S} (note that both A1{T}() and A1{S}() constructors are readily available) defined on the abstract type for use construction in a struct that holds a value A1{T} with a specific T based on context (other field types).

Currently I have defined the Base.convert method like this

Base.convert(::Type{AbstractA{S}}, a::A) where {S,A<:AbstractA} = eval(A.name.name){S}()

This works as intended

julia> convert(AbstractA{Float64}, A1{Int32}())
A1{Float64}()

but this seems like quite a hack. My first question is, is there any better (more idiomatic) way to do this?

It also doesn’t solve the problem of conversion. I have a composite type defined as

struct Composite{T, P<:AbstractArray{T}, A<:AbstractA{T}}
    t::P
    a::A
    Composite(aa::AbstractArray, a::AbstractA)= new{eltype(aa),typeof(aa),typeof(a)}(aa, a)
end

but calling the Composite constructor with different parameter types for P and A fails

julia> Composite([5.0], A1())
ERROR: TypeError: in Composite, in A, expected A<:AbstractA{Float64}, got Type{A1{Any}}
[...]

I am not surprised by this since how would the constructor know, which of Float64 and Any to prefer. My question is, how to define the constructor correctly so that the conversion works?

A question for better understanding: How does parameter type promotion in type constructors work and can I modify the precedence by defining methods of Base.promote_type?

The A.name.name is an internal detail which is not guaranteed to work. It’s best to avoid it. However, I don’t think there’s a documented way to retrieve the A1 from an object A1{S}().
What’s needed to solve it is constructions like fun(a::T{S}) where {T<:AbstractA, S} ..., but that’s not supported. Yet.

Unless you need to create subtypes of AbstractA on the fly, I suggest to make a convert method for each subtype. If one needs to create more, it can be embedded in some function to create the necessary converts and constructors:

abstract type AbstractA{T} end

const subtypes = (:A1, :A2)
for subtype in subtypes
    @eval begin
        struct $subtype{T} <: AbstractA{T} end
        $subtype() = $subtype{Any}()
        Base.convert(::Type{AbstractA{S}}, a::$subtype) where S = $subtype{S}()
    end
end

The Composite constructor suffers from the same problem. It’s probably better to avoid the object A1() as an argument, and stick to just the type name A1:

struct Composite{T, P<:AbstractArray{T}, A<:AbstractA{T}}
    t::P
    a::A
    function Composite(aa::AbstractArray{T}, ::Type{AT}) where {T, AT <: AbstractA}
        typ = AT{T}
        new{eltype(aa),typeof(aa),typ}(aa, typ())
    end
end

Composite([5.0], A1)

Alternatively, you can create an outer constructor for each subtype, and call the inner constructor with invoke (to avoid infinite recursion):

struct Composite{T, P<:AbstractArray{T}, A<:AbstractA{T}}
    t::P
    a::A
    Composite(aa::AbstractArray{T}, a::AbstractA{T}) where {T} = new{T, typeof(aa), typeof(a)}(aa,a)
end

for subtype in subtypes
    @eval begin
        function Composite(aa::AbstractArray{T}, a::$subtype) where {T}
            newa = convert(AbstractA{T}, a)
            invoke(Composite, Tuple{typeof(aa), AbstractA{T}}, aa, newa)
        end
    end
end

Composite([5.0], A1())

There might exist more elegant solutions, perhaps someone knows?

1 Like

There’s also an alternative to create a function atype for each subtype, which returns the type A1, A2 etc:

abstract type AbstractA{T} end

const subtypes = (:A1, :A2)

for subtype in subtypes
    @eval begin
        struct $subtype{T} <: AbstractA{T} end
        $subtype() = $subtype{Any}()
        atype(::$subtype) = $subtype
    end
end

Base.convert(::Type{AbstractA{S}}, a::AbstractA) where {S} = atype(a){S}()

struct Composite{T, P<:AbstractArray{T}, A<:AbstractA{T}}
    t::P
    a::A
    function Composite(aa::AbstractArray{T}, a::AbstractA) where T
        newa = convert(AbstractA{T}, a)
        new{eltype(aa),typeof(aa),typeof(newa)}(aa, newa)
    end
end

julia> Composite([5.0], A1())
1 Like

I prefer this method.
I solved the problem with composite construction with a combination of an inner constructor and outer constructors that call the convert directly.
For these types, I have

struct Composite{T, P<:AbstractArray{T}, A<:AbstractA{T}}
    t::P
    a::A
    Composite{T}(aa::AbstractArray, a::AbstractA) where {T} = new{T, typeof(aa), typeof(a)}(aa,a)
end
Composite(aa::AbstractArray{S}, a::AbstractA) where {S} = Composite{S}(aa, convert(AbstractA{S}, a))

Does this method have any drawbacks? I like to avoid using invoke when not necessary since I have a hard time reasoning about it.