# Catching tuples with types in them

I’m trying to dispatch a method acting on tuples where one of the elements is a type and ran into difficulties because of some unexpected behaviour.

First, here’s behaviour I thought I understood:

julia> (:x, 4) isa Tuple{Symbol,Integer}
true


This is despite the fact that typeof(4) is not Integer but only a subtype of Integer. Okay.

But then this caught me by surprise:

julia> (:x, Int) isa Tuple{Symbol,Type{Int}}
false


despite

julia> Int isa Type{Int}
true


What am I missing here? Or, what is the correct way to understand the first example that is consistent with the behaviour observed in the first?

And what do should I use for U in f(::U) so that f catches all objects of the form (s, Int) for some s <: Symbol?

1 Like

Int is a type, whereas 4 is an element of a type Int
typeof(Int) == DataType

1 Like

catch em all

function pokemon(s::Symbol, t::Type{Int})
return (Symbol, Type{Int})
end

function pokemon(s::Symbol, t::Type{T}) where {T<:Signed}
return (Symbol, (T, Signed))
end

function pokemon(s::Symbol, t::Type{<:Integer})
return (Symbol, Type{<:Integer})
end

pokemon(:symbol, Int)
# (Symbol, Type{Int64})

jpokemon(:symbol, Int16)
# (Symbol, (Int16, Signed))

pokemon(:symbol, UInt16)
# (Symbol, Type{#s5} where #s5<:Integer)

pokemon(:symbol, Integer)
# (Symbol, Type{#s5} where #s5<:Integer)

1 Like

In case that wasn’t clear from previous answers:

# define an auxiliary method that works on two arguments: both args are
# matched individually, and you can use the Type{Int} pseudo-type
_f(s::Symbol, ::Type{Int}) = "OK"

# define your "real" function f, which takes a single tuple as argument
# (but splats it and delegates to _f)
f(u) = _f(u...)


This catches tuples of any symbol with Int:

julia> u = (:x, Int)
(:x, Int64)

julia> f(u)
"OK"


but not tuples containing any other type:

julia> v = (:x, Float64)
(:x, Float64)

julia> f(v)
ERROR: MethodError: no method matching _f(::Symbol, ::Type{Float64})
Closest candidates are:
_f(::Symbol, ::Type{Int64}) at REPL[1]:1
Stacktrace:
[1] f(::Tuple{Symbol,DataType}) at ./REPL[2]:1
[2] top-level scope at REPL[6]:1

3 Likes

Thanks both for your responses. I think your workaround @JeffreySarnoff is clever (because I also thought of itmyself ) but it is awkward, you have to admit.

Returning to my original confusion, is it correct, then, that object::T is successful exactly when typeof(object) <: T? And that Tuple{A1,A2} <: Tuple{B1,B2} is true if and only if A1 <: B1 and A2 <: B2? Evidently not, for while this explains why my second example fails, according to the first assertion, Int::Type{Int} should fail also (because DataType <: Type{Int} is false). But it doesn’t.

So, reformulating my original inquiry, what’s the correct interpretation of :: that is simultaneously consistent with all three of the following observed behaviours:

(:x, 4)::Tuple{Symbol,Integer} # succeeds
(:x, Int)::Tuple{Symbol,Type{Int}} # fails
Int::Type{Int} # succeeds


While I have no explanation to give you (and hopefully someone more knowledgeable will chime in to give you one), here are some thoughts about this topic.

1. I think it is correct to say that a::T (in the context of typeassert) is successful if and only if a isa T. I also think it is true in the context of multiple dispatch.

Like you, I used to think that:
\quad a isa T \Leftrightarrow typeof(a) <: T
and I still think this holds for most values a, but there are exceptions and you just found one:

julia> Int isa Type{Int}
true

julia> typeof(Int)
DataType

julia> DataType <: Type{Int}
false

julia> Type{Int} <: DataType
true


I.e. for most values a, typeof(a) is the “smallest” type T for which a isa T is true. But since all types share the same type (DataType) and it is useful to dispatch on the value of types, the “pseudo-type” (or singleton type) Type{T} has been introduced for the purposes of dispatch, and it does not follow this rule.

2. An other thing that might be relevant here is the variance of tuples: while it is true that parametric types are invariant

julia> Vector{Int} <: Vector{Number}
false # although Int <: Number


tuples are a bit exceptional in that they are covariant:

julia> Tuple{Int, Int} <: Tuple{Int, Number}
true


Again, I think I recall someone saying here that this is because tuples are used internally to represent arguments in method calls, and this behavior simplified the implementation of multiple dispatch.

So I guess it all boils down to the question of knowing exactly how these two exceptions combine. If we think first about the covariance of tuple types, then we get your interpretation:
\quad (4, Int) isa Tuple{Int, Type{Int}}
= 4 isa Int && Int isa Type{Int}
= true

However, we could also think first about pseudo-types and say, for example: x isa T if typeof(x) <: T or (x is a type and T == Type{x}). Such an interpretation is widely different in this case:
\quad (4, Int) isa Tuple{Int, Type{Int}}
= typeof((4, Int)) <: Tuple{Int, Type{Int}} (the exception does not apply)
= Tuple{Int, DataType} <: Tuple{Int, Type{Int}}
= Int <: Int && DataType <: Type{Int} (covariance of tuple types)
= false

I’m fully aware that I did not give any real answer here; I just wanted to share my thoughts. In conclusion, I would rather phrase the question like this: if both Type{T} and tuple type covariance are features that were introduced to help with multiple dispatch, how come the dispatch on two arguments does not work exactly in the same way as the dispatch on a tuple of arguments?

2 Likes