Parametric method type matching issue

Consider this function

foo(x::T, y::S) where {T <: Integer, S <: T} = 1

I read it as

  1. y has a type of S
  2. type S should be a subtype of T
  3. type T should be a subtype of Integer

But, I can do

julia> foo(1, 0x00)
1

T is Int64, S is UInt8, but S is not a subtype of T. So, why would it dispatch? It seems that it’s enforcing S to be a subtype of Integer rather than a subtype of T.

What would be a subtype of T? If T is concrete there are no subtypes apart from T itself.

Yes, that would be the behavior that I expect i.e. S == T, when T is concrete.

Technically, Union{} or Base.Bottom is the bottom type for all types but that not useful in this context.

No. T is undefined.

T is known when the method is called. Isn’t it dynamic?

No. There are infinite number of T that can match so T is not known at any time.

julia> foo(x::T, y::S) where {T <: Integer, S <: T} = (T, S)
foo (generic function with 1 method)

julia> foo(1, 0x00)
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] foo(::Int64, ::UInt8) at ./REPL[1]:1
 [2] top-level scope at none:0

What does this mean?

At the call site, I’m passing two arguments and they have type Int64 and UInt8 respectively. If I just match them to foo’s signature then T would be Int64 and S would be UInt8, except that it does not satisfy the where condition properly because Int64 <: Integer but UInt8 <: Int64 is not true.

I see that T is not defined in your example above but I don’t understand why it complains about that. Can you elaborate why there are infinite number of T that can match?

When I said dynamic, I was just referring to dynamic dispatch where at runtime the types of all arguments are known.

But

julia> foo(x::T, y::S) where {T <: Integer, S <: T} = (T, S)
foo (generic function with 1 method)

julia> foo(1,1)
(Int64, Int64)

No T could be anything that is a supertype of Int64 and S could be anything that is a supertype of UInt8, there are infinite many of those.

It’s the same reason f(::T, ::Vector{T}) where T can be called with f(1, []). You can’t say in this case that you first match the ::T to T = Int since that’ll conflict with the ::Vector{T}. In this case T = Any in the end.

Because there are infinite number of types that is supertype of Int64.

I believe you are confused about the special rule about covariant type parameters. I don’t know what’s the exact definition of the rule right now (in corner cases, like when x and y have the same time shown in @giordano’s post ) but it says that if a type parameter only appears in covariant location then it must be a concrete type. This is so that (::T, ::T) where T won’t be the same as (::Any, ::Any) and (::Tuple{T,T}) where T won’t be the same as (::Tuple{Any,Any}). The rule does not apply since you are using the type parameter elsewhere in this case.

  1. Dynamic dispatch means the dispatch is done at runtime, it’s in contrast to static dispatch and both are purely optimization concept and has nothing to do with the semantics. In fact, if not for all your calls are in the global scope, the dispatch is certainly going to be static.
  2. The types of the argument of course must be know by dispatch time (which must be no latter than runtime) but that does not mean anything about type parameters. They don’t need to be assigned.
2 Likes

Thanks for the additional details. I have found the documentation about Diagonal Types and it has cleared up some of my misunderstanding.

How about this? Even though T is undefined, S is determined to be UInt8… what makes it possible to return S here?

julia> goo(x::T, y::S) where {T <: Integer, S <: T} = S
goo (generic function with 1 method)

julia> goo(1, 0x00)
UInt8
1 Like

Because S can only be a concrete type here