Notice: a subtle footgun: UnionAll type variable order + method static parameter normalization

Consider this snippet:

id(::Type{T}) where {T} = T
some_type{7,Int} == id(some_type){7,Int}

Naively, it seems like the comparison on the second line should return true for any type some_type, assuming neither type application throws. After all, id is the identity function, so the second line should be equivalent to the obvious tautology some_type{7,Int} == some_type{7,Int}, right?

Well, id is the identity in the ==-sense, but not in the ===-sense. And == between UnionAll types isn’t affected by type variable order. Thus:

julia> some_type = AbstractArray{T,N} where {N,T}  # note the reversed type variable order
AbstractArray{T, N} where {N, T}

julia> id(::Type{T}) where {T} = T
id (generic function with 1 method)

julia> some_type{7,Int} == id(some_type){7,Int}

julia> some_type == id(some_type)

julia> some_type === id(some_type)

julia> AbstractArray === id(some_type)

julia> some_type === identity(some_type)

So id(some_unionall) effectively normalizes the type, and thus the unionall type variable order. I guess my conclusion here is to take care to do the normalization as soon as possible, to prevent shooting myself in the foot.

EDIT: a similar example:

julia> T = AbstractArray{T,N} where {N,T}
AbstractArray{T, N} where {N, T}

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

julia> f(T)
1 Like

Seems to happen right at parameterization:

julia> some_type = AbstractArray{T,N} where {N,T}
AbstractArray{T, N} where {N, T}

julia> Type{some_type}

julia> Vector{some_type} # any parametric type does this
Vector{AbstractArray} (alias for Array{AbstractArray, 1})

julia> id(::Type{some_type}) = some_type;

julia> methods(id)
# 1 method for generic function "id" from Main:
 [1] id(::Type{AbstractArray})
     @ REPL[11]:1

julia> id(AbstractArray) # well that's weird
AbstractArray{T, N} where {N, T}

julia> identity(AbstractArray) # phew, thanks to identity(@nospecialize x)

julia> identity(some_type) # phew: the sequel
AbstractArray{T, N} where {N, T}

Maybe it’s worth clarifying in the type parameter docs that parameters are normalized for whatever reason, or whether that’s even a language semantic or a fringe type implementation detail.

EDIT: I’m pretty sure concrete types can’t be == and !==, but evidently not all such abstract types get normalized as parameters. Might be nice to know which and why.

julia> Tuple{Union{Int,Bool}} == Union{Tuple{Int}, Tuple{Bool}}

julia> Tuple{Union{Int,Bool}} === Union{Tuple{Int}, Tuple{Bool}}

julia> Type{Tuple{Union{Int,Bool}}}
Type{Tuple{Union{Bool, Int64}}}

julia> Type{Union{Tuple{Int}, Tuple{Bool}}}
Type{Union{Tuple{Bool}, Tuple{Int64}}}

Normalization also happens at method argument annotations, maybe because they are parameters of an underlying tuple for the method?

julia> foo(::some_type) = 0
foo (generic function with 1 method)

julia> methods(foo)
# 1 method for generic function "foo" from Main:
 [1] foo(::AbstractArray)
     @ REPL[4]:1
1 Like