Dispatch using Union and order of arguments

question

#1

I tried to subtract a type T from a union type with this function (a "poor man’s Core.Compiler.typesubtract"):

typesubtract(::Type{Union{T,S}}, ::Type{T}) where {S, T} = S
typesubtract(::Type{S}, ::Type{T}) where {S, T} = S
typesubtract(::Type{T}, ::Type{T}) where {T} = Union{}

However, this function did not work for simple case like typesubtract(Union{Missing, Float64, Int}, Missing) which returns Union{Missing, Float64, Int64} while I expected Union{Float64, Int}.

On the other hand, just flipping arguments as the function union_poptype works (This is taken from @galenlynch’s PR #30549):

union_poptype(::Type{T}, ::Type{Union{T,S}}) where {T, S} = S
union_poptype(::Type{T}, ::Type{S}) where {T, S} = S
union_poptype(::Type{T}, ::Type{T}) where {T} = Union{}

Why union_poptype works and my typesubtract doesn’t? Does Julia’s dispatch system depend on order of arguments? I thought the type system prefers to throw ambiguity error rather than using argument order to break ties.


I checked those functions using

using Test

@testset "union_poptype" begin
    @test union_poptype(Missing, Union{Missing, Float64, Int}) == Union{Float64, Int}
    @test union_poptype(Missing, Union{Missing, Float64}) == Float64
    @test union_poptype(Missing, Union{Float64, Int}) == Union{Float64, Int}
    @test union_poptype(Missing, Float64) == Float64
end

@testset "typesubtract" begin
    @test typesubtract(Union{Missing, Float64, Int}, Missing) == Union{Float64, Int}
    @test typesubtract(Union{Missing, Float64}, Missing) == Float64
    @test typesubtract(Union{Float64, Int}, Missing) == Union{Float64, Int}
    @test typesubtract(Float64, Missing) == Float64
end

which yields

Test Summary: | Pass  Total
union_poptype |    4      4
typesubtract: Test Failed at ...
  Expression: typesubtract(Union{Missing, Float64, Int}, Missing) == Union{Float64, Int}
   Evaluated: Union{Missing, Float64, Int64} == Union{Float64, Int64}
  ...
typesubtract: Test Failed at ...
  Expression: typesubtract(Union{Missing, Float64}, Missing) == Float64
   Evaluated: Union{Missing, Float64} == Float64
Stacktrace:
  ...
Test Summary: | Pass  Fail  Total
typesubtract  |    2     2      4

#2

Does reversing the order of T and S inside the where {T, S} make any difference?


#3

No difference. Flipping the type parameters in where of typesubtract and union_poptype doesn’t change the result.


#4

I think in this case

typesubtract(::Type{Union{T,S}}, ::Type{T}) where {S, T} = S

is not matched for the call

typesubtract(Union{Missing, Float64, Int}, Missing)

You can check this by defining just this method. I suspect this is a bug, but I am an expert on types and dispatch, hopefully someone will clarify this.

Also, I can make it match with :>, but

julia> typesubtract(::Type{>: Union{T,S}}, ::Type{T}) where {S,T} = S
typesubtract (generic function with 1 method)

julia> typesubtract(Union{Missing, Float64, Int}, Missing)
ERROR: UndefVarError: S not defined
Stacktrace:
 [1] typesubtract(::Type{Union{Missing, Float64, Int64}}, ::Type{Missing}) at ./REPL[1]:1
 [2] top-level scope at REPL[2]:1

looks like a bug.


#5

Ah, that’s a clever trick. And you are right. The method does not match:

julia> f(::Type{Union{T,S}}, ::Type{T}) where {S, T} = S
f (generic function with 1 method)

julia> f(Union{Missing, Float64, Int}, Missing)
ERROR: MethodError: no method matching f(::Type{Union{Missing, Float64, Int64}}, ::Type{Missing})
Closest candidates are:
  f(::Type{Union{T, S}}, ::Type{T}) where {S, T} at REPL[2]:1
Stacktrace:
 [1] top-level scope at REPL[3]:1

julia> g(::Type{T}, ::Type{Union{T,S}}) where {S, T} = S
g (generic function with 1 method)

julia> g(Missing, Union{Missing, Float64, Int})
Union{Float64, Int64}

This is nice. I didn’t realize :> can be used in this case.

But it looks like S not defined part is a known issue (or in a way “by design”). Jeff Bezanson says:

There are various cases where we can’t determine the value of a static parameter, and we decided it was better to give errors in those cases than to affect method matching
https://github.com/JuliaLang/julia/issues/30713#issuecomment-454129552


#6

I opened the issue here: https://github.com/JuliaLang/julia/issues/30751

BTW, I noticed that it works in Julia 0.6.4

julia> f(::Type{Union{T,S}}, ::Type{T}) where {S, T} = S
f (generic function with 1 method)

julia> f(Union{Void, Float64, Int}, Void)
Union{Float64, Int64}

julia> g(::Type{T}, ::Type{Union{T,S}}) where {S, T} = S
g (generic function with 1 method)

julia> g(Void, Union{Void, Float64, Int})
Union{Float64, Int64}