Parametric type to force common container type

I want to define a type that holds one vector of Float64 and another of ComplexF64, but I want their containers are of the same type. For example, I want them to be Vector{Float64} and Vector{ComplexF64}, or CUDA.CuVector{Float64} and CUDA.CuVector{ComlpexF64}, etc.

Initially I hoped something like this to work:

struct MyWrongType{V<:AbstractVector}
    vr::V{Float64}
    vc::V{ComplexF64}

    MyWrongType{V}(r,c,n) where {V<:AbstractVector} = new(fill(r,n), fill(c,n))
end

but this generates an error (related threads: Cannot define parametric type with parametric field, Type definition):

ERROR: TypeError: in Type{...} expression, expected UnionAll, got a value of type TypeVar

A working approach is something like

struct MyType{VR<:AbstractVector{Float64},VC<:AbstractVector{ComplexF64}}
    vr::VR
    vc::VC

    MyType{VR,VC}(r,c,n) where {VR<:AbstractVector{Float64},VC<:AbstractVector{ComplexF64}} = new(fill(r,n), fill(c,n))
end

but this does not force VR and VC to be of the same container type.

The most satisfactory solution I came up with is to use the above working approach and additionally to define an outer constructor that takes the container type V as a constructor argument (rather than a type parameter):

MyType(V, r, c, n) = MyType{V{Float64},V{ComplexF64}}(r,c,n)

Then I can do things like

using StaticArrays, CUDA

m_vector = MyType(Vector, 0.0, 0.0im, 10)
m_static = MyType(SVector{10}, 0.0, 0.0im, 10)
m_cuda = MyType(CuVector, 0.0, 0.0im, 10)

Each of these instances holds real and complex vectors of the same container type.

I’m considering taking this approach, but wondering if there is a better, more standard approach to solve the described problem.

4 Likes

I am not sure why you want to do this (I expect no performance or other benefit).

I would just go with something like

struct MyType{VR,VC}
    vr::VR
    vc::VC
end

and define outer constructors to use Vector, SVector, etc.

1 Like

Because I don’t want to define those individual outer constructors for different container types. You don’t want to push new code to the repository and ask the users to pull it whenever you need to support a new container type.

I guess it would be nice if one could write:

struct MyType{T, C<:AbstractVector, VR<:C{T}, VC<:C{Complex{T}}}
    vr::VR
    vc::VC
end

(Only the T part works currently.)

Update: Hm, thinking about it, this probably isn’t possible because AbstractVector is AbstractVector{X} where X. So C would never be just, say, Vector but Vector{X}.)

The problem is that in general, this is not feasible or even well-defined. See the discussions related to Base.similar, which solves a similar (:wink:) problem.

This, again, is not possible, without a common API for constructing <:AbstractVector instances, which does not exist at the moment. Eg as you have noticed, SVectors require the length in the type.

It is possible. Not sure if you read my original posting till the end, but the point is that I’ve found a workaround to solve this problem. I wanted to share my solution with other people as I saw similar questions multiple times in this forum and elsewhere. I am happy with my solution, but was just wondering if there is a better, more standard approach.

Your solution just happens to work for SVector because the type signature is aligned that way, ie SVector{10}{Float64} happens to be meaningful.

But if someone had a type where the parameter list happens to be arranged differently (which is perfectly possible, as the interface does not require this), it wouldn’t work, which is why it is not generic.

This is true, but the users can always define a type alias such that the last type parameter is an element type as

const AliasType{...,T} = OriginalType{...,T,...}

where T is the element type. For example, SMatrix{S1,S2,T,L} is in such a problematic format, but we can define

const MySMatrix{S1,S2,L,T} = SMatrix{S1,S2,T,L}

The user can call MyType with V = MySMatrix{3,3,9} when SMatrix needs to be used.

Do you need the types to be the same for some kind of compiler optimization or anything? This doesn’t really solve your problem so much as sidestep it, but if you wanted to avoid very ornate type system games, you could always just put an assert in the constructor that enforces vc and vr being the same container type.

2 Likes
struct Foo{VT, ET, FV, CV}
    fv::FV
    cv::CV

    function Foo(fv::FV, cv::CV) where {FV, CV}
        VT = typejoin(FV, CV)
        @assert !isabstracttype(VT)
        @assert VT <: AbstractVector
        ET = eltype(CV)
        @assert eltype(CV) === Complex{eltype(FV)}
        new{VT, ET, FV, CV}(fv, cv)
    end
end

Should something like this work? It’ll have a long repr, but you can do things like:

using SparseArrays

f = Foo([1., 2., 3.], ComplexF64[1., 2., 3.])

@assert f isa Foo{Vector, Float64}

sf = Foo(sprand(Float32, 10, .1), sprand(ComplexF32, 10, .2))

@assert sf isa Foo{<:SparseVector, Float32}

There should be no downsides to type inference, and the inner constructor pretty much compiles away.