Why does eltype not return upper type bound?

My code just stumbled over eltype(Array{F} where {F <: Integer}) returning Any instead of Integer. Has this just not been implemented yet (i.e., should I open an issue?), or are there good reasons for this behavior that I am not aware of?

Observed on julia 1.9.3 and 1.10-rc1

You’re falling back to the eltype(::Type) = Any method, instead of the eltype(::Type{<:AbstractArray{E}}) where {E} method for arrays or the eltype(::Type{T}) where T<:Number method for numerical scalars, etc.

Array{F} where {F <: Integer} is a UnionAll of multiple concrete types Vector{Integer}, Vector{Union{Int32, Int64}}, Vector{Int16}... that do not narrow down to 1 element type, so returning Integer would be misleading. It could make sense for another function to extract UnionAll type bounds.

And as far as I know, there’s no way to make a type bound a method parameter e.g. function ltype(::Type{Array{F} where {F <: T}) where T T end, so to implement that you’d have to define each method one by one.

1 Like

In a given concrete case one could define something like

f(::Type{<:Array{F}}) where F = F
f(::Type{T}) where T = T.var.ub   # for UnionAll type

Then

julia> f(Vector{Int})
Int64

julia> f(Array{F} where {F <: Integer})
Integer

but that might not be a good idea in general.

1 Like

Also, @nospecialize can provide a way:

julia> f(@nospecialize v::Vector{T}) where {T <: Real} = T
f (generic function with 1 method)

julia> f([1,2])
Int64

julia> f(Real[1,2])
Real

but this might be abuse of the macro.

That would work without the @nospecialize, and that is not what OP is aiming for. You’re passing in Vector instances, OP is passing in a UnionAll instance.

The dispatch would be iffy when we consider types other than Arrays. For example, f(Int) would throw an error. Maybe just f(T::UnionAll) = T.var.ub?

1 Like

I just wanted to give the general idea. There are certainly ways to improve my example.

Thanks for these details! I suspected that I hit upon a fallback method that just defaulted to Any.

However, I am not sure if I would consider returning Integer misleading here. From an interface point of view: wouldn’t it make sense if eltype(T) aspires to return the tightest type F (that can be determined) such that the elements when iterating over values of type T are guaranteed to be of type F? And the T.var.ub hack shows that the information is (in principle) available, even though it is not exposed conveniently, so a generic implementation may be unfeasible.

No because 1 concrete type’s parameters can be concrete or abstract, and an abstract parameter is not a type bound e.g. eltype(Array{Integer}) == Integer. eltype is already established to retrieve a parameter, so you need a different function for bounds, or perhaps one that makes it ambiguous whether it’s a type parameter or bound. Bear in mind that Array{Integer} is also an instance of UnionAll, so that type alone can’t distinguish parameter from bound.

Manipulating type bounds doesn’t fit dispatch rules: 1) only UnionAll instances can involve type bounds, but we mainly pass non-type instances into methods, 2) a method parameter is only matched with type parameters, not bounds, and requires the call provide 1 unambiguous value for it. We can reach into the unguaranteed internals for that kind of inconvenience. As an example of inconvenience, let’s keep talking about the type bound of the element type in a UnionAll. Unfortunately, Array{Integer}.var.ub == Any; the problem for us there is that T was fixed, so we’re seeing a type bound for N, which isn’t even a type. We could check for T, but there’s no guarantee that a type’s element type parameter is named T. This could probably still be pulled off with an additional method mapping declared types to the names of their element type parameters e.g. Array to T, and falling back to the eltype when the element type is found to be fixed.