Specificity of binary method with diagonal type specification for dispatch

I am having trouble understanding why the following code does not dispatch on Base.convert(::Type{T}, ::T) as I would expect:

abstract type SuperType end

struct MyType{T} <: SuperType end

function Base.convert(::Type{<:MyType}, t::SuperType)
  println("Converting")
  MyType{Int}()
end

convert(MyType{Int32}, MyType{Int32}())
# goes through the `convert` method we just added instead of `Base.convert(::Type{T}, ::T)`

Is there a reason for this behavior? It forces me to define Base.convert(::Type{T}, t::T) where {T<:SuperType} = t for the code to avoid bugs that rely on this invariant.

1 Like

I second this question. Appreciate if someone can clarify what is happening here.

This behavior seems reasonable to me. The Base.convert(::Type{<:MyType}, t::SuperType) method that you defined is more specific than the Base.convert(::Type{T}, ::T) method. The general rule is that when two methods are both applicable, the more specific method is the one that will be called.

In fact, the Base.convert method that you defined explicitly contradicts the invariant that you’re trying to maintain. So you could define something like this instead:

function Base.convert(::Type{S}, x::T) where {S <: MyType, T <: SuperType}
    println("Converting")
    if S == T
        x
    else
        MyType{Int}()
    end
end

However, that still seems goofy, because then you have

convert(MyType{Int32}, MyType{Int8}()) == MyType{Int64}

which violates the invariant that typeof(convert(T, x)) == T.

What exactly is it that you are trying to achieve with your new convert method?

Unfortunately the rules for method specificity are not given in the Julia manual. I think the details are rather complicated, so the developers do not want to codify a list of specificity rules by putting them in the manual. There’s a brief discussion of method specificity in the developer docs:

https://docs.julialang.org/en/v1/devdocs/types/#Subtyping-and-method-sorting

Also take a look at this video:

Thanks for your answers. I would have expected that the diagonal type specification (having (::Type{T}, ::T) where {T}) would have been more specific over anything else (unless a similar method whose T is further constrained was defined of course).

What the convert method does is not important, I admit that in this example it may not make much sense. In your code snippet, the line if S == T is the equivalent of Base.convert(::Type{T}, t::T) where {T<:SuperType} = t I mentioned above. I would have preferred to avoid any of both, but I guess this is just a corner case that is not so intuitive for method specificity rules.

I think subtype relationships have the highest priority when it comes to method specificity. Diagonal type restrictions are farther down on the list.

In your actual use case, are you defining a convert method, or is it some other function? Just wondering, because usually if you encounter a method error for convert, it doesn’t mean that you should implement a convert method—it means that you are doing something wrong further up the chain. :slight_smile:

1 Like

I see, it makes sense, thanks.

It is indeed about a convert method, which actually might be better specified with the form

Base.convert(::Type{MyType}, t::SuperType)

instead of

Base.convert(::Type{<:MyType}, t::SuperType)

which removes the issue.

1 Like