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.