# \`Union\` constraints in abstract type declarations seem to break subtype relations

**URL:** <https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322>\
**Category:** General Usage\
**Created:** [February 16, 2024, 10:34pm UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322 "2024-02-16T22:34:39Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![brainandforce](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/brainandforce/32/211054_2.png) [@brainandforce](https://discourse.julialang.org/u/brainandforce)\
**Post date:** [February 16, 2024, 10:34pm UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/1 "2024-02-16T22:34:39Z")

</div>

I’m currently writing a package, [CliffordNumbers.jl](https://github.com/brainandforce/CliffordNumbers.jl), that makes the following abstract type declaration:

```julia
const BaseNumber = Union{Real,Complex}

abstract type AbstractCliffordNumber{Q<:QuadraticForm,T<:BaseNumber} <: Number
end

```

Clifford algebras are defined over real or complex numbers, and I wouldn’t want to include other objects that subtype `Number` outside of the `Real` and `Complex` types provided by `Base`, such as a `Quaternion` type. In the code, I define an alias to simplify this.

I’ve defined a concrete type:

```julia
struct CliffordNumber{Q,T,L} <: AbstractCliffordNumber{Q,T}
    data::NTuple{L,T}
end

```

but I get some very unexpected results when I work with subtyping relationships:

```julia-repl
julia> supertype(CliffordNumber)
AbstractCliffordNumber{Q,T} where {Q,T}

```

I’d think that `AbstractCliffordNumber` would be sufficient, but the results get stranger from here:

```julia-repl
julia> CliffordNumber <: AbstractCliffordNumber
false

julia> CliffordNumber{APS} <: AbstractCliffordNumber{APS}
false

julia> CliffordNumber{APS,Float64} <: AbstractCliffordNumber{APS,Float64}
true

```

From my inspection of the behavior, it seems like Julia may not infer that the type constraints on `T` in `AbstractCliffordNumber{Q,T}` propagate to its subtypes if `T` is a `Union`, because this works:

```julia-repl
julia> CliffordNumber{APS,<:Any} <: AbstractCliffordNumber{APS}
true

julia> CliffordNumber{APS,<:Union{Real,Complex}} <: AbstractCliffordNumber{APS}
true

```

Is this a bug in the implementation of subtyping, or am I doing something unreasonable enough to generate these results?

---

<div class="post-metadata">

**Author:** ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)\
**Post date:** [February 16, 2024, 11:48pm UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/2 "2024-02-16T23:48:43Z")

</div>

What happens if you change `BaseNumber` to a non-`Union` type?

---

<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:** [February 17, 2024, 4:06am UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/3 "2024-02-17T04:06:15Z")

</div>

> [@brainandforce](#):
>
> I’d think that `AbstractCliffordNumber` would be sufficient

`supertype` computes a less constrained result because your `CliffordNumber` definition doesn’t restrict `Q` or `T` like `AbstractCliffordNumber` does. If you try to make a `CliffordNumber` type outside those constraints though, `AbstractCliffordNumber` does throw a `TypeError`, so this statement is still practically correct. Here’s another example of a parametric type and its parametric supertype’s type constraints not lining up:

```julia
julia> struct Realvec{T<:Real} <: AbstractVector{T} end

julia> supertype(Realvec)
AbstractVector{T} where T<:Real

```

Though in that case the constraint is narrower on the subtype’s parameter.

> [@brainandforce](#):
>
> ```julia
> julia> CliffordNumber <: AbstractCliffordNumber
> false
> 
> julia> CliffordNumber{APS} <: AbstractCliffordNumber{APS}
> false
> 
> ```

Same reasoning, `CliffordNumber`’s parameters are less constrained to the type system.

> [@brainandforce](#):
>
> ```julia
> julia> CliffordNumber{APS,Float64} <: AbstractCliffordNumber{APS,Float64}
> true
> 
> ```

This one works nicely because you fixed the type parameters with the constraints mismatch.

> [@brainandforce](#):
>
> ```julia
> julia> CliffordNumber{APS,<:Any} <: AbstractCliffordNumber{APS}
> true
> 
> ```

Strange, I get `false`, which makes sense because the 2nd `CliffordNumber` parameter isn’t constrained. I didn’t install your package, I only copy-pasted `struct QuadraticForm{P,Q,R} end; const APS = QuadraticForm{3,0,0}` and then the code you posted, but AFAIK this shouldn’t make a difference because type definitions are set in stone.

> [@brainandforce](#):
>
> ```julia
> julia> CliffordNumber{APS,<:Union{Real,Complex}} <: AbstractCliffordNumber{APS}
> true
> 
> ```

This one works for me, and it makes sense because the 2nd `CliffordNumber` parameter has the same constraint as `AbstractCliffordNumber`.

As for why a subtype’s parameters doesn’t gain the constraints of its declared supertype’s parameters by default, it’s because you actually implicitly specified `Any` as the type constraints. It’s not treated differently from doing:

```julia
abstract type AB{T<:Real} end
struct B{T<:AbstractString} <: AB{T} end
#= same as specifying arbitrary where clause in iterated union,
which isn't restricted by an existing type's where clause,
in this case B{T} where T<:AbstractString also specifies
a supertype AB{T} where T<:AbstractString (matching T). =#

```

It’s more apparent there that you can’t simply “inherit” parameter constraints, you must compute the type intersection; in `B`’s case there is no valid type parameter. `typeintersect` is documented to be occasionally imprecise (though it works perfectly for `typeintersect(AbstractString, Real) == Union{}`), so checking for a valid parameter is done using the direct types’ and its supertypes’ constraints sequentially. If you don’t want to deal with intersection difficulties, manually replicate or narrow constraints in all the subtypes’ definitions. This is best practice because you don’t want to look up supertypes, which are possibly in other packages, just to know the parameter constraints.

---

<div class="post-metadata">

**Author:** ![brainandforce](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/brainandforce/32/211054_2.png) [@brainandforce](https://discourse.julialang.org/u/brainandforce)\
**Post date:** [February 18, 2024, 8:37pm UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/4 "2024-02-18T20:37:54Z")

</div>

> [@Benny](#):
>
> As for why a subtype’s parameters doesn’t gain the constraints of its declared supertype’s parameters by default, it’s because you actually implicitly specified `Any` as the type constraints.

> [@Benny](#):
>
> If you don’t want to deal with intersection difficulties, manually replicate or narrow constraints in all the subtypes’ definitions. This is best practice because you don’t want to look up supertypes, which are possibly in other packages, just to know the parameter constraints.

This explains it. This might be something that needs to be reflected in Julia documentation, because I couldn’t find anything on it in the Types section, even though it does cover parametric abstract types.

---

<div class="post-metadata">

**Author:** ![brainandforce](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/brainandforce/32/211054_2.png) [@brainandforce](https://discourse.julialang.org/u/brainandforce)\
**Post date:** [February 27, 2024, 9:28pm UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/5 "2024-02-27T21:28:29Z")

</div>

Just going to point out that I’ve [opened up an issue on GitHub](https://github.com/JuliaLang/julia/issues/53380) to have this behavior documented.

> [@Benny](#):
>
> If you don’t want to deal with intersection difficulties, manually replicate or narrow constraints in all the subtypes’ definitions. This is best practice because you don’t want to look up supertypes, which are possibly in other packages, just to know the parameter constraints.

The one response does suggest enforcing the constraint through `typeintersect`.

---

<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:** [February 28, 2024, 5:01am UTC](https://discourse.julialang.org/t/union-constraints-in-abstract-type-declarations-seem-to-break-subtype-relations/110322/6 "2024-02-28T05:01:04Z")

</div>

Unfortunately I don’t think that’ll work because of `typeintersect`’s occasional imprecision, checking type parameter constraints up the supertype chain really is more reliable, and that’s not something you can really store in 1 `UnionAll`. There could be a method that prints the sequence of parameter constraints, for reflection’s sake.

Besides, making types have straightforward type parameter constraints in the source code still requires manual design, for example `struct CliffordNumber{Q,T,L}` would be misleading whether or not the resulting `UnionAll` instance incorporated perfected `typeintersect` constraints. That would indeed be nice to document somewhere.

Feel free to bring these up in the issue or just link this thread in your post, that’s done sometimes for brevity’s sake.
