# Coupling finite\`Union\` with \`T\` in \`NTuple{2, T} where {T}\`

**URL:** <https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567>\
**Category:** General Usage\
**Tags:** tuple, parametric-types, functions, union\
**Created:** [October 31, 2025, 12:16am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567 "2025-10-31T00:16:25Z")\
**Posts on this page:** 8\
**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:** [October 31, 2025, 12:16am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/1 "2025-10-31T00:16:25Z")

</div>

We know that for `UnionAll` of `Tuple` that uses the same type parameter (e.g., `T`) for all the element types (in default) requires the element types to be the same concrete type:

```julia-auto
julia> f1(a::Tuple{T, T}) where {T<:Real} = a
f1 (generic function with 1 method)

julia> f1((1, 1))
(1, 1)

julia> f1((1, 1.0))
ERROR: MethodError: no method matching f1(::Tuple{Int64, Float64})
The function `f1` exists, but no method is defined for this combination of argument types.

```

I was hoping that if I replace `T` with a **finite** `Union` of types parameterized by `T`, the requirement of `T` to be the same concrete type still holds:

```julia-auto
julia> f2(a::Tuple{Union{T, Complex{T}}, Union{T, Complex{T}}}) where {T<:Real} = a
f2 (generic function with 1 method)

julia> f2((1, 1.0)) # Expect this to prompt `MethodError` assuming `T` iterate through all possible concrete types

```

But that’s not the case:

```julia-auto
julia> f2((1, 1.0)) # This actually is legal as `T==Real` somehow is allowed
(1, 1.0)

```

Is this consistent with the design of `UnionAll`?

If so, how can I parameterize the type signature of `f2` to allow each element of `a` to be either `T` or `Complex{T}`, but must have the same concrete `T`? In other words, any `(T1, T2)` in `Tuple{Union{T1, Complex{T1}}, Union{T2, Complex{T2}}}` such that `T1!==T2 || !isconcretetype(T1)` should be disallowed.

Thanks!

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [October 31, 2025, 11:23am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/2 "2025-10-31T11:23:13Z")

</div>

Consider

```julia-auto
julia> typ = Union{ Tuple{T,T} where T<:Real, Tuple{T,Complex{T}} where T<:Real, Tuple{Complex{T}, Complex{T}} where T<:Real, Tuple{Complex{T}, T} where T<:Real }

```

Note that this means that you cannot pattern-match / extract T.

But this is a nice example!

And it imo demonstrates why the diagonal rule kinda sucks. (it’s awesome syntax, but it would be clearer if we kept it as syntactic sugar for something like `Tuple{T,T} where T<: CONCRETE`).

---

<div class="post-metadata">

**Author:** ![sgaure](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sgaure/32/14779_2.png) [@sgaure](https://discourse.julialang.org/u/sgaure)\
**Post date:** [October 31, 2025, 11:41am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/3 "2025-10-31T11:41:39Z")

</div>

> [@frankwswang](#):
>
> Is this consistent with the design of `UnionAll`?

Yes. It’s `Tuple` and `Union` which are specially treated in the sense that if a type variable occurs more than once and _only_ inside `Tuple` and `Union` (in “covariant position”), it is required to be concrete.

> **[More about types · The Julia Language](https://docs.julialang.org/en/v1/devdocs/types/#Diagonal-types)**
>
> Documentation for The Julia Language.

> [@frankwswang](#):
>
> If so, how can I parameterize the type signature of `f2` to allow each element of `a` to be either `T` or `Complex{T}`, but must have the same concrete `T`?

Perhaps this:

```julia-auto
f2(a::Tuple{Union{R,Complex{C}}, Union{R,Complex{C}}}) where {R, R<:C<:R} = a

```

Edit: no:

```julia-auto
julia> f2((1,Complex{Real}(1.0, 1.0)))
(1, 1.0 + 1.0im)

```

---

<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:** [October 31, 2025, 1:28pm UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/4 "2025-10-31T13:28:10Z")

</div>

Things get strange when type parameters are allowed to not be concrete types. It makes sense to accommodate abstract type parameters sometimes:

```julia-auto
julia> foo(a::T, ::Vector{T}) where T = (a, T)
foo (generic function with 1 method)

julia> foo(1.0, Real[0])
(1.0, Real)

```

but if you modify `f2` to return `(a, T)`, you don’t actually get `T==Real` because it’s not the only or narrowest supertype:

```julia-auto
julia> f2((1, 1.0))
ERROR: UndefVarError: `T` not defined in static parameter matching
Suggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.

```

which I’m not even certain is intended.  
A weirder example:

```julia-auto
julia> f3(a::Tuple{T, Complex{T}}) where T = (a, @isdefined(T) ? T : "undefined")
f3 (generic function with 1 method)

julia> f3((1, 1.0im))
ERROR: MethodError: no method matching f3(::Tuple{Int64, ComplexF64})
...
julia> f4(a::Tuple{T, Union{T, Complex{T}}}) where T = (a, @isdefined(T) ? T : "undefined")
f4 (generic function with 1 method)

julia> f4((1, 1.0))
((1, 1.0), "undefined")

julia> f4((1, 1.0im))
((1, 0.0 + 1.0im), "undefined")

julia> f5(a::Tuple{T, Union{T, Complex{T}}}, ::Vector{T}) where T = (a, @isdefined(T) ? T : "undefined")
f5 (generic function with 1 method)

julia> f5((1, 1.0), Real[])
((1, 1.0), Real)

julia> f5((1, 1.0im), Real[])
ERROR: MethodError: no method matching f5(::Tuple{Int64, ComplexF64}, ::Vector{Real})
...

```

I don’t know why the concrete diagonal rule applies to `f3` when `Complex{T}` is in all cases, or why it’s not always possible to force a known `T` with another argument’s type parameter.

The previously linked source about the diagonal rule does imply that a design alternative can demand a concrete `T` in `foo(a::T, ::Vector{S}) where {T, T<:S}`. I’m inclined to prefer a concrete-except-abstract-parameters rule, but I don’t know if that falls apart somewhere.

---

<div class="post-metadata">

**Author:** ![FerreolS](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ferreols/32/216854_2.png) [@FerreolS](https://discourse.julialang.org/u/FerreolS)\
**Post date:** [November 2, 2025, 9:56pm UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/5 "2025-11-02T21:56:59Z")

</div>

This is a bit counter intuitive

> If so, how can I parameterize the type signature of `f2` to allow each element of `a` to be either `T` or `Complex{T}`, but must have the same concrete `T`? In other words, any `(T1, T2)` in `Tuple{Union{T1, Complex{T1}}, Union{T2, Complex{T2}}}` such that `T1!==T2 || !isconcretetype(T1)` should be disallowed.

This can be implemented using some traits mechanism:

```julia
julia> struct Foo{d} end

julia> Foo{true}(a) = a

julia> f4(a::Tuple{Union{T1, Complex{T1}}, Union{T2, Complex{T2}}}) where {T1 <: Real, T2<: Real} = Foo{T1==T2 && isconcretetype(T1)}(a)
f4 (generic function with 1 method)

julia> f4((1,1.0))
ERROR: MethodError: no method matching Foo{false}(::Tuple{Int64, Float64})
The type `Foo{false}` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  (::Type{Foo{d}} where d)()
   @ Main REPL[1]:1

Stacktrace:
 [1] f4(a::Tuple{Int64, Float64})
   @ Main ./REPL[2]:1
 [2] top-level scope
   @ REPL[4]:1

julia> f4((1,1))
(1, 1)

julia> f4((1.,1im))
ERROR: MethodError: no method matching Foo{false}(::Tuple{Float64, Complex{Int64}})
The type `Foo{false}` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  (::Type{Foo{d}} where d)()
   @ Main REPL[1]:1

Stacktrace:
 [1] f4(a::Tuple{Float64, Complex{Int64}})
   @ Main ./REPL[2]:1
 [2] top-level scope
   @ REPL[6]:1

julia> f4((1.,1.0im))
(1.0, 0.0 + 1.0im)
```

---

<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:** [November 10, 2025, 10:19pm UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/6 "2025-11-10T22:19:59Z")

</div>

Thanks for the answer! Your solution definitely works for the example I gave in the OP.

Unfortunately, in my actual use case, the number of elements in the `Tuple`-type argument `a::Tuple{T, Vararg{T, N}} where {T, N}` can vary, and I want to have a universal constraint for all `N`. In other words, what I actually have is something like this:

```julia-auto
f3(a::Tuple{Union{T, Complex{T}}, Vararg{Union{T, Complex{T}}, N}}) where {T<:Real, N} = a

```

Therefore, I cannot simply write down all the combinations of types to form a concrete union.

Honestly, this issue I encountered made me think that Julia’s support for constructing well-constrained and decidable subtypes from `UnionAll` is still incomplete.

---

<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:** [November 11, 2025, 7:53am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/7 "2025-11-11T07:53:30Z")

</div>

It seems that as soon as parameterized invariant types are mixed with covariant types, the rules of diagonal types (or “diagonal-type parameters” `T` more specifically) become ill-defined. This is the only description of when the diagonal type parameters should be restricted to concrete types (a.k.a, a “concrete-type restriction”) I found in the [official documentation](https://docs.julialang.org/en/v1/devdocs/types/#Diagonal-types):

> It turns out that being able to dispatch on whether two values have the same type is very useful (this is used by the promotion system for example), so we have multiple reasons to want a different interpretation of `Tuple{T,T} where T` . To make this work we add the following rule to subtyping: if a variable occurs more than once in covariant position, it is restricted to ranging over only concrete types. (“Covariant position” means that only `Tuple` and `Union` types occur between an occurrence of a variable and the `UnionAll` type that introduces it.) Such variables are called “diagonal variables” or “concrete variables”.

However, this so-called “covariant position” does not seem to be a sufficient condition to enforce the concrete-type restriction since `f3` in your example does not allow `f3((1, 1.0im))`:

> [@Benny](#):
>
> ```julia-auto
> julia> f3(a::Tuple{T, Complex{T}}) where T = (a, @isdefined(T) ? T : "undefined")
> f3 (generic function with 1 method)
> 
> julia> f3((1, 1.0im))
> ERROR: MethodError: no method matching f3(::Tuple{Int64, ComplexF64})
> 
> ```

even though `Complex{T}` is an invariant type.

One the other hand, when `Complex{T}` is mixed with `Union`, , e.g., for `f4`, somehow `T` no longer has the concrete-type restriction…

> [@Benny](#):
>
> ```julia-auto
> julia> f4(a::Tuple{T, Union{T, Complex{T}}}) where T = (a, @isdefined(T) ? T : "undefined")
> f4 (generic function with 1 method)
> 
> julia> f4((1, 1.0))
> ((1, 1.0), "undefined")
> 
> ```

To verify this inconsistency isn’t a special quirk about `Complex`, but is really about the mixing of covariant types and `Union`, we can also test the following cases:

```julia-auto
julia> f3_2(a::Tuple{T, Vector{T}}) where {T} = a
f3_2 (generic function with 1 method)

julia> f3_2((1, [1.0]))
ERROR: MethodError: no method matching f3_2(::Tuple{Int64, Vector{Float64}})
The function `f3_2` exists, but no method is defined for this combination of argument types.

julia> f4_2(a::Tuple{T, Union{T, Vector{T}}}) where {T} = a
f4_2 (generic function with 1 method)

julia> f4_2((1, [1.0]))
(1, [1.0])

```

Overall, I think it’s worth to **at least** improve the documentation of when the concrete-type restriction takes effect for the type parameters in the diagonal types, if the existing inconsistent behavior is not deemed “problematic”.

---

<div class="post-metadata">

**Author:** ![foobar\_lv2](https://avatars.discourse-cdn.com/v4/letter/f/ee59a6/32.png) [@foobar\_lv2](https://discourse.julialang.org/u/foobar_lv2)\
**Post date:** [November 11, 2025, 10:48am UTC](https://discourse.julialang.org/t/coupling-finite-union-with-t-in-ntuple-2-t-where-t/133567/8 "2025-11-11T10:48:15Z")

</div>

> [@frankwswang](#):
>
> To make this work we add the following rule to subtyping: if a variable occurs more than once in covariant position, it is restricted to ranging over only concrete types

Oh, that’s just incomplete, as visible from `(1, 1.0, Real[]) isa Tuple{T,T,Vector{T}} where T`. It has to be:

> if a variable occurs more than once in covariant position and never in invariant position, it is restricted to ranging over only concrete types

In many other documentation places, this is indeed stated clearly.

The thing that makes your example very tricky is how `Union` interacts with that rule. For example,

```julia-auto
$ julia +1.0 -e "println((1, 1.0) isa Union{Vector{T}, Tuple{T,T}} where T)"
false
$ julia +1.4 -e "println((1, 1.0) isa Union{Vector{T}, Tuple{T,T}} where T)"
false
$ julia +1.5.0 -e "println((1, 1.0) isa Union{Vector{T}, Tuple{T,T}} where T)"
true
$ julia +1.12 -e "println((1, 1.0) isa Union{Vector{T}, Tuple{T,T}} where T)"
true

```
