Consider a simple example where I define a type alias for (non-empty) Tuple:
julia> const MyTuple1{T, N} = Tuple{T, Vararg{T, N}}
Tuple{T, Vararg{T, N}} where {T, N}
Even when the type parameter T is not assigned, the UnionAll-type MyTuple1 refers to all the instances of MyTuple1{T, N} where T and N are both concrete type parameters (meaning they are either concrete types or literal value like 1). This can be verified with the following example:
julia> t1 = Tuple{Int, Float64}
Tuple{Int64, Float64}
julia> t1 <: MyTuple1{Real}
true
julia> MyTuple1{Real} == Tuple{Real, Vararg{Real}}
true
julia> t1 <: MyTuple1
false
When T in MyTuple1 was unassigned, it included all possible concrete types but not abstract types like Real. Hence, even though t1 is a subtype of MyTuple1{Real}, it’s not a subtype of MyTuple1.
Such an implicit constraint on type parameters still holds when one type parameter is used as an upper bound for another type parameter:
julia> const MyTuple2{U, T<:U, N} = Tuple{T, Vararg{T, N}}
Tuple{T, Vararg{T, N}} where {U, T<:U, N}
julia> t1 <: MyTuple2
false
julia> t1 <: MyTuple2{Real}
false
julia> t1 <: MyTuple2{Real, Real}
true
Even when the upper-bound parameter U is set to Real, if T is unassigned, it does not include Real, unless I manually set it to be Real.
However, there are edge cases that break this implicit constraint on type parameters.
For example, when the type parameter is used as a lower bound, the bounded type parameter seems to be allowed to include abstract types when unassigned:
julia> const MyTuple3{U, T>:U, N} = Tuple{T, Vararg{T, N}}
Tuple{T, Vararg{T, N}} where {U, T>:U, N}
julia> t1 <: MyTuple3
true
So, my question is: Is this behavior expected?
Additionally, what’s more strange is the following paradoxical behaviors:
julia> t1 <: MyTuple3{Real}
true
julia> t1 <: MyTuple3{Float64}
false
julia> t1 <: MyTuple3{Float64, Real}
true
It appears that by setting U to be a concrete type Float64, T fell back to referring to all possible concrete types. I don’t know if this will be classified as a bug, but it’s definitely a bizarre behavior of the type system (on Julia 1.11.3).
Okay, what about having type parameters defined for both an upper and a lower bound? Here is the result:
julia> const MyTuple4{U, L<:U, L<:T<:U, N} = Tuple{L, Vararg{T, N}}
Tuple{L, Vararg{T, N}} where {U, L<:U, L<:T<:U, N}
julia> t1 <: MyTuple4
true
julia> t1 <: MyTuple4{Real}
true
julia> t1 <: MyTuple4{Real, Real}
true
julia> t1 <: MyTuple4{Real, Float64}
false
julia> t1 <: MyTuple4{Real, Real}
true
It seems that the “fall-back effect” of a concrete lower-bound parameter still persists, whereas when both U and L are unassigned, they do include abstract-type instances.