# Unassigned type parameters in \`UnionAll\` of \`Tuple\` do not refer to concrete types in edge cases

**URL:** <https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217>\
**Category:** General Usage\
**Tags:** type, tuple, parametric-types, diagonal-rule\
**Created:** [January 26, 2025, 7:02am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217 "2025-01-26T07:02:36Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [January 26, 2025, 7:02am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/1 "2025-01-26T07:02:36Z")

</div>

Consider a simple example where I define a type alias for (non-empty) `Tuple`:

```julia
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
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
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
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
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
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.

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [January 26, 2025, 8:51am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/2 "2025-01-26T08:51:07Z")

</div>

Regarding the constraint that restricts a type parameter of an `UnionAll` to concrete types, it’s called the _diagonal rule_. The open-access paper _Julia Subtyping: A Rational Reconstruction_ (with DOI `10.1145/3276483`) describes it like so:

> This rule requires that if a type variable appears more than once in covariant position, it can be instantiated only with a _concrete_ type

In more detail later in the paper:

> A variable is said to be in _covariant position_ if only `Tuple`, `Union`, and `UnionAll` type constructors occur between an occurrence of the variable and the `where` construct that introduces it. The _diagonal rule_ states that if a variable occurs more than once in covariant position, and never in invariant position, then it is restricted to ranging over only concrete types.

The diagonal rule is also discussed in the Julia documentation, though only in the devdocs, for some reason: [Diagonal types](https://docs.julialang.org/en/v1/devdocs/types/#Diagonal-types).

Regarding the unexpected behavior you point out: before publishing the paper, the authors of the paper made a suggestion to the Julia authors to relax the diagonal rule regarding lower bounds for pragmatic reasons:

> <https://github.com/JuliaLang/julia/issues/26453>
>
> If I understand correctly, right now the rule for \`Tuple{T,T} where T\>:t\` is: \`T…\` is considered \*\*diagonal\*\* unless it is \*\*obvious that \`t\` is abstract\*\*. Therefore, in the examples below the types on the right are not diagonal:
> \`\`\`
> julia\> (Tuple{A,A} where A\>:Integer) \<: Tuple{T,T} where T\>:Number
> true
> 
> julia\> (Tuple{A,A,Number} where A\>:Number) \<: Tuple{T,T,S} where T\>:S where S\>:Integer
> true
> \`\`\`
> In the following modification of the previous example, however, \`T\` is considered diagonal despite the fact that its lower bound \`S\` \_can be abstract\_.
> \`\`\`
> julia\> (Tuple{A,A,Number} where A\>:Number) \<: Tuple{T,T,S} where T\>:S where S
> false
> \`\`\`
> 
> I totally see that it is a hard task to determine concreteness of types involving variables. What I would suggest is to change the strategy for \`Tuple{T,T} where T\>:t\` and consider \`T\` \*\*non-diagonal\*\* unless it is \*\*obvious that \`t\` is concrete\*\*. I think that if programmers write \_lower\_ bounds, it is likely that they really want to switch off the diagonal rule.

This is also discussed in the paper itself, at the bottom of the paper, under _Issues reported to Julia developers_:

> Whenever the programmer specifies explicitly a lower bound for a type-variable, as in `Tuple{T,T}where T>:t`, it is not always easy to decide if `T` should be considered diagonal or not. This depends on whether the lower bound, `t`, is concrete, but in general deciding concreteness is hard and Julia implementation approximates it with an heuristic. We proposed that the variables should be considered diagonal only if their concreteness is obvious. The proposal was approved, implemented and merged into the _master_ branch.

---

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [January 26, 2025, 11:13pm UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/3 "2025-01-26T23:13:30Z")

</div>

Thank you for providing relevant materials! They are very informative!

I understand it’s more pragmatic for `T` to be considered non-diagonal (i.e., including abstract types in `Tuple`) when an explicit lower bound parameter is used. I don’t understand why `T` should fall back to being diagonal when the lower bound is a concrete type:

```julia
julia> const MyTuple5{T, S} = Tuple{T, T, T} where {S, T>:S}
Tuple{T, T, T} where {S, T>:S}

julia> (Tuple{A, A, Number} where A>:Number) <: MyTuple5
true

julia> (Tuple{A, A, Number} where A>:Number) <: MyTuple5{Float64}
false

```

To me, just because `S` is set to be `Float64`, it does not mean I would want `T` also to be concrete. Because if that’s the case, I would have just used `NTuple{3, T}` instead of `MyTuple5` in the first place. Isn’t it assumed that a type that can be a supertype of a concrete `S` (and `S` is not set to be `Union{}`) should be inferred as an abstract type? If there isn’t some implementation obstacle, I don’t see why we can’t lift off the diagonal-type rule for `T` even when `S` is assigned a concrete type. (I’d like to learn more if there is any!)

Moreover, the current convention for switching on/off the diagonal-type rule for `T` seems inconsistent in the following case:

```julia
julia> (Tuple{A, A, Number} where A>:Number) <: Tuple{T, T, T} where {T>:Float64}
false

julia> (Tuple{A, A, Number} where A>:Number) <: Tuple{T, T, T} where {S>:Float64, T>:S}
true

```

In the first line, the diagonal-type rule is turned on when `T` is lower-bounded by a concrete type `Float64`. However, in the second line, when `T` is bounded by `S`, which should be diagonal (i.e., only allowed to refer to any concrete types) as it is bounded by `Float64`, granted `T` the permission to refer to abstract types…

I suppose the cause behind such a consequence could be that `S where {S>:Float64}` is deemed an abstract type, so the diagonal-type rule is then turned off for `T` when it’s bounded by `S`. But if the diagonal-type rule is so easily turned off (rather than persisting across all type parameters iteratively as long as the initial bound enforces the diagonal-type rule), then why do we need to enforce it for a type parameter bounded by a literal concrete type in the first place?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [January 27, 2025, 7:31am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/4 "2025-01-27T07:31:31Z")

</div>

TBT to this lower bound of a `Tuple` parameter messing with me:

```julia
julia> Tuple{Int} <: (Tuple{T} where T>:Real) # huh?
true

julia> Tuple{Any} <: (Tuple{T} where T>:Real) # obviously
true

julia> Tuple{Int} <: Tuple{Any} # obviously...oh right, covariance
true

```

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [January 29, 2025, 3:20am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/5 "2025-01-29T03:20:00Z")

</div>

> [@frankwswang](#):
>
> If there isn’t some implementation obstacle, I don’t see why we can’t lift off the diagonal-type rule for `T` even when `S` is assigned a concrete type.

I think it may be way too late for making that change, as explained here [PSA: Julia is not at that stage of development anymore](https://discourse.julialang.org/t/psa-julia-is-not-at-that-stage-of-development-anymore/44872).

If you really want to make this change, you could make a PR. Assuming it passes the Julia test suite, it could be further evaluated with PkgEval, to see which registered packages’ test suites does it break. Hypothetically it could get accepted as a _minor change_ if it’s not too breaking, however I think you’d have to present a stronger motivation than given here.

It might be interesting to see what would such a PR break in the ecosystem 😂

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [January 30, 2025, 8:14am UTC](https://discourse.julialang.org/t/unassigned-type-parameters-in-unionall-of-tuple-do-not-refer-to-concrete-types-in-edge-cases/125217/6 "2025-01-30T08:14:13Z")

</div>

> [@frankwswang](#):
>
> Moreover, the current convention for switching on/off the diagonal-type rule for `T` seems inconsistent in the following case:
> 
> ```julia
> julia> (Tuple{A, A, Number} where A>:Number) <: Tuple{T, T, T} where {T>:Float64}
> false
> 
> julia> (Tuple{A, A, Number} where A>:Number) <: Tuple{T, T, T} where {S>:Float64, T>:S}
> true
> 
> ```

I have no idea what’s going on but here are more comparisons. Also works with just 2 parameters if that’s a bit shorter to write.

```julia
julia> (NTuple{3, T} where T<:AbstractFloat) == (NTuple{3, T} where {S<:AbstractFloat,T<:S})
true

julia> (NTuple{3, T} where T>:AbstractFloat) == (NTuple{3, T} where {S>:AbstractFloat,T>:S}) == NTuple{3, Any}
true

```

Which intuitively suggests that the value of the upper bound `T=Any` makes the “element” `NTuple{3, Any}` a trivial equal set that overcomes the diagonal rule due to Tuple parameter covariance. However we need that indirect parameter to do the same with a concrete lower bound:

```julia
julia> (NTuple{3, T} where T>:Float64) == (NTuple{3, T} where {S>:Float64,T>:S})
false

julia> (NTuple{3, T} where {T>:Float64}) <: (NTuple{3, T} where {S>:Float64, T>:S}) == NTuple{3, Any}
true

julia> Tuple{AbstractFloat, Float64, Float64} <: (NTuple{3, T} where {T>:Float64})
false

```

and as far as it preserving the diagonal rule, I’m just not sure, the only element I stumbled into is `NTuple{3, Float64}` but it’s not equal to it either:

```julia
julia> NTuple{3, Float64} <: (NTuple{3, T} where T>:Float64)
true

julia> NTuple{3, AbstractFloat} <: (NTuple{3, T} where T>:Float64)
false

julia> NTuple{3, Real} <: (NTuple{3, T} where T>:Float64)
false

julia> NTuple{3, Float64} == (NTuple{3, T} where T>:Float64)
false

```
