Strange MethodError ambiguity

I found a strange MethodError in my code. It is possible to reproduce it with the following simple code

struct A{T} x::T end
fun(a::A{T}, b::T) where {T} = "same parametric type"
fun(a::T, b::S) where {T<:A,S} = "any parametric type"
fun(b::S, a::T) where {T<:A,S} = "any parametric type"

Then, fun(A(A(1)), A(1)) returns an ambiguity where I expected it to return the string "same parametric type".

Does someone know if this is the expected behaviour? From my (certainly limited) understanding of the multiple dispatch, I do not see why this is ambiguous… (btw I use Julia 1.4.1)

EDIT: it seems that

f1(a::A{T}, b::T) where {T} = "same parametric type"
f1(a::T, b::S) where {T<:A,S} = "any parametric type"

does not yield methods ambiguity. However,

f2(a::A{T}, b::T) where {T} = "same parametric type"
f2(b::S, a::T) where {T<:A,S} = "any parametric type"

is enough to throw the MethodError ambiguity. Moreover, the Possible fixsuggested by Julia is

Possible fix, define f2(::S, ::T) where {T<:A, S<:(A{T} where T)}

which to me seems superfluous.

Julia’s multiple dispatch mechanism works by selecting the most specific method given your arguments. The problem when calling f2(A(A(1)), A(1)) is that neither methods are more specific than the other.
A{T} is more specific than S for the first argument A(A(1)), but T where T<:A is more specific for the second argument A(1), hence the MethodError ambiguity.

I completely agree about your first paragraph; hence my question!

Although, your second paragraph suggests that Julia only looks at the arguments “one at a time”; this was not my understanding… When looking at both the arguments, the choice of the method is obvious.

My understanding is that a method with argument type A is more specific than a function with argument type B if A <: B. For the two methods

f(a::A{T}, b::T) where {T} = "same parametric type"
f(b, a::A) = "any parametric type"

the argument types are Tuple{A{T},T} where T and Tuple{Any,A}. It is clear that

julia> (Tuple{A{T},T} where T) >: Tuple{Any,A}
false

because of types like Tuple{Int,A{Float64}} which is a subset of the latter but not of the former. Furthermore, we also have

julia> (Tuple{A{T},T} where T) <: Tuple{Any,A}
false

because of types like Tuple{A{Int},Int} which is a subset of the former but not of the latter. Hence neither method is more specific.

2 Likes

Interesting. In fact I would expect your ordering to work as well…

It just seems that I misunderstood how the multiple dispatch works.

If

h(::Type{A{T}},::Type{T}) where {T} = true
h(::Type{S},::Type{T}) where {S,T<:A} = false
julia> h(A{A{Int}},A{Int})
ERROR: MethodError: h(::Type{A{A{Int64}}}, ::Type{A{Int64}}) is ambiguous. Candidates:
  h(::Type{A{T}}, ::Type{T}) where T in Main at none:1
  h(::Type{S}, ::Type{T}) where {S, T<:A} in Main at none:1
Possible fix, define
  h(::Type{A{T}}, ::Type{T}) where T<:A

I agree that (Tuple{A{T},T} where T<:A) <: Tuple{Any,A} and (Tuple{A{T},T} where T<:A) <: (Tuple{A{T},T} where T) should both be true.

However, observe that for h(A{A{Int}},A{Int}) satisfies the definition h(::Type{A{T}},::Type{T}) where {T} two times : the first argument matches the expected type A{T} and the parameter of the first argument T which is the type of the second argument also matches.

Meanwhile, our second definition of h is only satisfied by a match with the second argument.

As a matter of fact, the fact that the possible fix given by Julia is to define

h(::Type{A{T}}, ::Type{T}) where T<:A

tells me that Julia kind of already know what to do!

EDIT: clearly Julia has a straightforward “set understanding” of the situation and returns the error as it is only concerned with the fact that Tuple{A{A{Int}},A{Int}} lies in the intersection of Tuple{Any,A} and Tuple{A{T},T} where T.

It might seem that the first definition is tighter, but the method will accept any T you want to throw at it. The second method, however, specifically limits the type of the second argument to only subtypes of A. From this perspective, it’s unclear which method is more specific: do we want to prioritize the requirement that the first argument is parametrized with the same type, or that the second argument must be subtyping A?
Hence Julia suggests the fix: define a method that enforces both requirements, which will be more specific than both previously defined methods.

1 Like

Thanks for the discussion! I agree.

I guess it is now a point of view to add this “priority feature” and perhaps not having this flexibility is actually much better and prevent weird edge cases / forces clarity / etc…