Wield behavior of multiple dispatch

In Julia 1.8.3. Given the following two type signatures.

(x::AbstractFloat, y::Number)
(x::Union{Float64, ComplexF64}, y::Union{Float64, ComplexF64})

I would expect there being an ambiguity error if the input type is (::Float64, ::Float64).

But

julia> FloatAndComplex64 = Union{Float64, ComplexF64}
Union{Float64, ComplexF64}

julia> begin
               function roughly_equal(x::AbstractFloat, y::Number)
                       @info "(::AbstractFloat, ::Number)"
                       -10 * eps(x) < x - y < 10 * eps(x)
               end
               function roughly_equal(x::Union{Float64, ComplexF64}, y::Union{Float64, ComplexF64})
                       @info "(::Union{Float64, ComplexF64}, ::Union{Float64, ComplexF64})"
                       abs(x - y) < 10 * eps(y)
               end
       end
roughly_equal (generic function with 4 methods)

julia> roughly_equal(3.0, 3.0)
[ Info: (::Union{Float64, ComplexF64}, ::Union{Float64, ComplexF64})
true

The rules for multiple dispatch are quite complex. Apparently, here the compiler ranks a union of concrete types higher than abstract types.

1 Like

Yes, the compiler considers the union to be more specific.

julia> Base.morespecific(Union{Float64, ComplexF64}, AbstractFloat)
true

julia> Base.morespecific(Union{Float64, ComplexF64}, Number)
true
4 Likes

On second thought, this can be understood using set theory: The union of concrete types is a subset of the abstract type because the abstract type includes any additional concrete subtype that is not included in the union.

Yeah, it makes sense. But it turns out the union of abstract types can also more concrete

julia> Base.morespecific(Union{AbstractFloat, Complex}, Real)
true

I gotta be honest that one makes no sense to me.

It also works with Union{Int, AbstractString} and Number.

This argument only applies to y:

julia> Union{Float64, ComplexF64} <: Number
true

But it does not hold for x:

julia> Union{Float64, ComplexF64} <: AbstractFloat
false
2 Likes