Type stability with a vector of abstracts inside struct

Hi all, I appreciate any help in making the following code type stable:

abstract type Something{T} end
Base.eltype(:: Something{T}) where T = T

mutable struct OneThing{T} <: Something{T}
  α :: T
end


# HERE
mutable struct ManyThings{T} <: Something{T}
  L :: Vector{Something}
end

# OR HERE
function ManyThings(L :: Vector{<: Something})
  T = promote_type(eltype.(L)...)
  ManyThings{T}(L)
end

function test()
  A = OneThing(2.0)
  B = OneThing(3)
  S = ManyThings([A, B])
  @code_warntype ManyThings([A, B])
end

test()

output:

┌ Info: 
└   eltype(S) = Float64
Variables
  #self#::Type{ManyThings}
  L::Array{OneThing,1}
  T::Any

Body::ManyThings{_A} where _A
1 ─ %1 = Base.broadcasted(Main.eltype, L)::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1},Nothing,typeof(eltype),Tuple{Array{OneThing,1}}}
│   %2 = Base.materialize(%1)::Any
│        (T = Core._apply(Main.promote_type, %2))
│   %4 = Core.apply_type(Main.ManyThings, T)::Type{ManyThings{_A}} where _A
│   %5 = (%4)(L)::ManyThings{_A} where _A
└──      return %5

Thanks in advance.

I am not sure what you want here: the L field of a ManyThings is not a concrete type, so how could the compiler infer more?

Also, note that eltype should be defined on types, not values.

Vector{Something} is not a concrete type, Vector{S} where S<:Something{T} would be. That would require the change of ManyThings parameters to ManyThings{T, S<:Something{T}}.
Another question: why is ManyThings mutable? Having a container as a single field sort of implies that you want to change the contents of said container, not re-bind L to something else.

Thanks for the replies.

@Tamas_Papp, my objective is to change ManyThings so that the compiler can infer more.
I changed eltype to eltype(:: Type{<: Something{T}}) = where T = T, is that the right way to do it?

@Vasily_Pisarev, there are two problems with Vector{S}: I can’t have S <: Something{T} because the element types are not the same (in the example, one is Float64, the other is Int). And if I only have S <: Something, then S is also not inferred. This is essentially the problem, I can’t have a concrete type for S, since promote_type(OneThing{Float64}, OneThing{Int}) = OneThing. Regarding mutable, it was a mistake on my part.

I got the following to be type stable, but I can’t extend it:

struct TwoThings{T,S,Q} <: Something{T}
  A :: S
  B :: Q
end

function TwoThings(A :: Something, B:: Something)
  T = promote_type(eltype(A), eltype(B))
  TwoThings{T,typeof(A),typeof(B)}(A, B)
end

Yes, that’s how you define eltype.

You may want something like

mutable struct ManyThings{T} <: Something{T}
    L::Vector{Something{T}} # note T
end

but it is hard to be sure from your problem description. Cf

https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-fields-with-abstract-type-1

You can define a custom promote and conversion rules like

Base.promote_rule(::Type{OneThing{T}}, ::Type{OneThing{S}}) where {T,S} = OneThing{promote_type(T, S)}

Base.convert(::Type{OneThing{T}}, x::OneThing{S}) where {T,S} = OneThing{T}(x.α)

Base.convert(::Type{OneThing{T}}, x::OneThing{T}) where T = x

if that suits your use case.
Of course, that would put OneThing(3.0) instead of the original OneThing(3) into ManyThings.
But once you put things into a container with an abstract element type, you lose the type stability, that’s inevitable.

Thanks, that makes sense. I think I’ll have to leave it type unstable, because my use case is a little more complicated (for instance, I have several other things <: Something{T}, and there is no clear way to promote them). If that slows down the code too much, I’ll take a second look.

You can still use function barriers:

https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions-1