Vector of partially concrete parametric types


#1

I thought I understood the where syntax but apparently I don’t. I have a parameterised structure along the lines of

struct MyStruct{T <: Number, F}
    u::Vector{T}
    f::F
end

and I’d like to have a vector of these structures such that the underlying numerical type is the same but the f field can be different (it’s a function). For example

v1 = MyStruct([1.0], sin)
v2 = MyStruct([2.0], cos)
v = [v1, v2]

In this case, as I understand it, the appropriate type signature is Vector{MyStruct{T, F} where F} where T and, as desired,

v isa Vector{MyStruct{T, F} where F} where T == true

However, if I just have a single element in the vector

vv = [v1]
vv isa Vector{MyStruct{T, F} where F} where T == false

but I don’t understand why.

I’d like to be able to handle both these cases. Is there a type signature that can handle both? (I know I can use a union with Vector{MyStruct{T, F}} where {T, F} but I was expecting the where syntax to be able to handle this directly.)


#2

A Vector{MyStruct{T, F} where F} where T has an element type which allows potentially different Fs for each element. When you do [v1] you’re instead getting Vector{MyStruct{Float64, typeof(sin)}} which has an element type which allows only that exact type. Those two Vector types are actually different types in Julia (because they have different element types).

Instead, you probably want, roughly, "A vector whose element type is some subtype of MyStruct{T, F} where F". You can write that as:

Vector{M} where {T, M <: (MyStruct{T, F} where F)}

or, more simply, since you can leave off trailing type parameters:

Vector{M} where {T, M <: MyStruct{T}}

or, even more simply (and the style I’d suggest):

Vector{<:MyStruct{T}} where T

For more info, see the discussion of covariant vs. invariant types here: https://docs.julialang.org/en/stable/manual/types/index.html#Parametric-Composite-Types-1


#3

As soon as you said this it became obvious what I was doing wrong. Thanks!


#4

Will this actually be any better (in terms of performance) than just defining the type as

struct MyStruct{T <: Number}
    u::Vector{T}
    f::Function
end

though?


#5

Yup, it will actually be better. ::Function is an abstract type, so access to the f field would be type-unstable in that case. Of course, this may not actually matter much, depending on how that field is used, so it’s always worth benchmarking to be sure.


#6

In this case it’s going to be in a hot loop where performance is critical hence going for the parameterized form. (Though by the time it hits the hot loop it’s also been unpacked into a tuple with some @generated magic to avoid dynamic dispatch entirely. The vector form is for user convenience as the problem is sequentially constructed.)

Also, I quite like the convenience of being able to use arbitrary callables (e.g. callable structs), which aren’t subtypes of Function, as well as regular functions.