# Vararg (sub)type parameter undocumented dispatch weirdness

``````a1(::Integer...) = nothing
a2(::I...) where {I <: Integer} = nothing
a3(::Vararg{Integer}) = nothing
a4(::Vararg{<:Integer}) = nothing
a5(::Vararg{I}) where {I <: Integer} = nothing
b3(::NTuple{n, Integer}) where {n} = nothing
b4(::NTuple{n, <:Integer}) where {n} = nothing
b5(::NTuple{n, I}) where {n, I <: Integer} = nothing

# Intended for calling the above with heterogeneous arguments
a(f) = f(zero(UInt8), zero(Int8))
b(f) = f(a(tuple))

a(a1)
a(a2)  # throws MethodError
a(a3)
a(a4)  # unexpectedly succeeds
a(a5)  # throws MethodError
b(b3)
b(b4)  # throws MethodError
b(b5)  # throws MethodError
``````

Why does `a(a4)` succeed? In particular, how is it different from `a(a5)` and from `b(b4)`?

2 Likes

`a4` requires the arguments to be `Integer`s. `a5` requires all arguments to be of the same type `I`, which in turn is a subtype of `Integer`.

So `a4` behaves the same as `a3` and `b3`.
But why does it not behave the same as `b4`, in both `a4` and `b4` we have `<:T` used in the place of the type parameter?

I think the answer is here:

``````julia> methods(b4)
# 1 method for generic function "b4":
 b4(::Tuple{Vararg{var"#s6", n}} where var"#s6"<:Integer) where n in Main at REPL:1
``````

The compiler internally assigns a name to the integer part in `b4`’s signature. So it ends up being the same as `a2` where the integer has to be the same concrete type.

1 Like

The difference is subtle: `a4` matches argument tuples of the type `Tuple{Vararg{T} where T <: Integer}`, while `a5` narrows to `Tuple{Vararg{T}} where T <: Integer`. When called on two or more arguments, the latter is a diagonal type that restricts T to range over only concrete types, hence the `MethodError`.

1 Like

OK, so it seems that I can call `methods(my_function)` to find out how the dispatch will behave for my function.

But does some consistent set of rules exist that I could learn to be able to predict this myself, because I’d like to avoid calling `methods` all the time?

EDIT: no, actually `methods` doesn’t even help me much, because it tells you almost nothing about `a4`, unlike with `b4`. And `a4` is the surprising one for me.

The easiest way to test dispatch behavior is with subtyping relations. Julia’s dispatch rule is very simple and consistent in this respect: the method selected has the most specific type signature that your argument is a subtype of. For example:

``````julia> x = zero(UInt8), zero(Int8)
(0x00, 0)

julia> typeof(x) <: Tuple{Vararg{<:Integer}}  # a4's method signature
true

julia> typeof(x) <: Tuple{Vararg{T}} where T<:Integer # a5's method signature
false
``````

The developer documentation is a good reference for Julia’s subtyping rules. For a deeper dive, this paper is a classic.

1 Like