Specialization on vararg of types

I don’t know if it was left out for some good reason, but the Vararg section is actually pretty vague about what is being specialized. Unlike lone functions and types, Vararg specifies multiple types with a possibly fixed arity. From what I could tell (though you should probably verify yourself), the “nonspecializing” __(x::Int...) specializes on Int just fine, it’s the arity it gives up on. Even __(x...) specializes on the first argument’s type in 2 specializations, one where the rest of the types match, and one where they are Any (or whatever abstract type annotates x). Making a method parameter __(x::T...) where T specializes on arguments matching 1 concrete type (and does not specialize on arity); this also specializes on input types like Type{Float32} already. Specifying the arity parameter, usually N, specializes over arity and every argument’s type.

That is, except for types themselves as inputs, which I still don’t understand. If consistent with non-type inputs, specializations should show their concrete types DataType (most types), Union (unions), UnionAll (iterated unions of parametric types), not the abstract Type. You say it doesn’t specialize, and for good reason, but it’s certainly not falling back to Any for the 2nd argument onward, either. It’s more like it specializes, but weirdly. Vararg{Type,N} doesn’t change anything because there’s no method parameter for type selection.

danielwe’s solution performs the type selection pattern for unlimited arguments in an unintuitive and creative way. It can be written fully as f_vararg(t::Vararg{Type{S} where S<:T}) where {T}, so it makes sense that each selected type S is a subtype of the 1 method parameter T<:Any. This also specializes over arity despite not specifying a method parameter for it, so this is indeed the equivalent to h_vararg(x::Vararg{Any, N}) where {N} for non-type inputs.

Other equivalents for completion:

  • __(t::Vararg{Type{Int}}) specifies a type but does not specialize on arity, like __(x::Int...) for non-type inputs.
  • no equivalents to __(x::T...) where T or __(x...) because a lone method parameter for the type also specializes on arity. Weird how x::T... didn’t specialize on arity too, but maybe the syntax mattered.
  • __(t::Vararg{Type{Int}, N}) where N specializes on arity and the specified type, like __(x::Vararg{Int, N}) where N for non-type inputs.
  • __(t::Vararg{Type{T}}) where {T} specializes on arity and 1 matching type, like __(x::Vararg{T,N}) where {T,N}, which also handles input types already.

In the vast majority of cases, a method parameter must be 1 known instance at compile-time for dispatch to work, even if it is absent from the body. I suppose an exception was spotted finally. Example below of the usual case:

julia> myeltype(::Type{S} where {S<:AbstractArray{T,N} where N}) where {T} = 42
myeltype (generic function with 1 method)

julia> myeltype(Union{Vector{Int}, Matrix{Int}}) # T is Int
42

julia> myeltype(Union{Vector{Int}, Vector{Bool}}) # T is unspecific
ERROR: MethodError: no method matching myeltype(::Type{Union{Vector{Bool}, Vector{Int64}}})

For posterity since this sort of compiler stuff might be version-dependent, this was all done on v1.10.0

3 Likes