Weirdness of `Type{Union{A,B}} where {B<:SuperB}`

I think my mental model of Type{Union{A,B}} is incorrect because I truly do not understand the following behavior:

julia> struct Foo end

julia> Foo isa (Type{Union{Foo,T}} where {T<:Base.IEEEFloat})
true

julia> Float32 isa (Type{Union{Foo,T}} where {T<:Base.IEEEFloat})
false

julia> Union{Foo,Float32} isa (Type{Union{Foo,T}} where {T<:Base.IEEEFloat})
true

Does anybody know what Type{Union{A,B}} where {B<:SuperB} stands for?

This is specifically cases where A is NOT a member of SuperB and hence I would not expect any unbound type parameters, because A by itself should not be a subtype of Type{Union{A,B}}.

It might make sense if this was expanded to Type{<:Union{A,B}}. But then I don’t understand why the union type is a member.

If this only matched Union{A,<:SuperB} explicitly, then I don’t understand why A matches.

cc @yebai @penelopeysm

Here’s a breakdown to explain each of your examples:

julia> struct Foo end

julia> S = Union{Foo, T} where {T<:Base.IEEEFloat}
Union{Foo, T} where T<:Union{Float16, Float32, Float64}

julia> T = Type{S{T}} where {T<:Base.IEEEFloat}
Type{Union{Foo, T}} where T<:Union{Float16, Float32, Float64}

julia> Foo isa T  # the first query in the OP, `true` because the following is `true`:
true

julia> Type{Foo} == T{Union{}}  # `true` because the following is `true`:
true

julia> Foo == S{Union{}}
true

julia> Float32 isa T  # the second query in the OP, `false` because there is no `X` such that `Type{Float32} == T{X}`, which is because there is no `X` such that `Float32 == S{X}`
false

julia> Union{Foo, Float32} isa T  # the third query in the OP, `true` because the following is `true`:
true

julia> Type{Union{Foo, Float32}} == T{Float32}  # `true` because the following is `true`:
true

julia> Union{Foo, Float32} == S{Float32}
true
4 Likes

Thanks!

Is there any way I can write this:

Union{Type{Union{Foo,Float16}},Type{Union{Foo,Float32}},Type{Union{Foo,Float64}}}

in a more efficient way using a where clause?

  • what do you want when you say “more efficient”? Perhaps you’d be satisfied by something like this:

    let f(@nospecialize x::Type) = Type{Union{Foo, x}}
        types = map(f, (Float16, Float32, Float64))
        Union{types...}
    end
    
  • Why do you want to use UnionAll (“a where clause”)? “Compression” (so to speak) of some Union with UnionAll will almost always, except in trivial cases, I think, produce a strict superset. I don’t know if that’s what you want. Could you say more about the subtyping (or other) properties of the type you’re looking for.

All I meant by “efficient” was if there was any way of repositioning the where clause that would generate Union{Type{Union{Foo,Float16}},... without me needing to write it out. But yeah seems there is not. I guess the difficulty is due the special behavior of Union!

I was asking for this: Support for `Union{Nothing, <:IEEEFloat}` by yebai · Pull Request #620 · chalk-lab/Mooncake.jl · GitHub. We were all surprised the Type{Union{A,B}} where {B} didn’t work but I guess in retrospect it makes sense.

Luckily we don’t have to match all T<:AbstractFloat so the solution is to just loop over the three Base.IEEEFloat.

Actually we do need to match

function tangent_type(::Type{Union{NoFData,Array{T,N}}}, ::Type{NoRData}) where {T<:Base.IEEEFloat,N}
    return Union{NoTangent,tangent_type(Array{T,N})}
end

I guess this might not be possible without also matching NoFData

Edit 1: Actually maybe it’s fine?? I guess isa injects a Union{} into all UnionAll but method dispatch does not?

Edit 2: [Am very confused]

What do you mean by “special behavior”?

No, that’s horrible. Instead of creating unnecessary duplicated methods, just use Union.

I don’t understand this last message at all. Keep in mind I’m not familiar with the internals of your package.

All I meant was the same thing you wrote above:

Which we can do for Base.IEEEFloat (because it’s an explicit union that we can loop through), but we seemingly can’t do for abstract types like AbstractFloat without an ambiguous form

foo(::Type{Union{Foo,B}}) where {B<:Base.IEEEFloat} = B

which also matches Type{A}:

julia> struct Foo end

julia> foo(::Type{Union{Foo,B}}) where {B<:Base.IEEEFloat} = B
foo (generic function with 1 method)

julia> foo(Foo)
Union{}

So the solution seems to be to make sure there is also a method for

foo(::Type{Foo}) = Foo

which disambiguates that case. And then I think we are fine to use the unbounded form without issue

Commented on the PR in question to show what I mean.

1 Like