# Defining parametric unions with no parametric members

I am trying to understand the syntax and requirements for defining parametric union types. It seems that defining a parametric union doesn’t work unless at least one of its components is also parametric. Example:

``````Foo{T <: Real} = Union{
Int,
Float64,
Vector{T},
}

Bar{T <: Real} = Union{
Int,
Float64,
}

fun_Foo(::Foo{T}) where {T} = println("foo!")
fun_Bar(::Bar{T}) where {T} = println("bar!")

fun_Foo(0)  #<--- works
fun_Bar(0)  #<--- does not work
``````

The reason is that `Foo{Int}` seems to be a valid type, but `Bar{Int}` is not. What is going on here?

The above is just an example to illustrate the problem, and I realise that in this example the solution is just to make `Bar` non-parametric. The real use case is that `Bar` is defining a collection of supported types that I gradually want to add to, and currently none of those types happen to be parametric. Future additions will be though, and I want to define the type such that it works either way.

I am using a workaround by defining something like

``````struct MyFakeType{T<:Real} end
``````

and then defining

``````Bar{T <: Real} = Union{
Int,
Float64,
MyFakeType{T}
}
``````

This seems somehow unsatisfactory though.

Just two data points:

``````fun_Bar(::Bar{T}) where {T} = println("bar!")
``````

with

``````ERROR: LoadError: TypeError: in Type{...} expression, expected UnionAll, got Type{Union{Float64, Int64}}
``````

on Julia 1.9.0-DEV.

• The following modification seems to work here
``````Foo{T <: Real} = Union{
Int,
Float64,
Vector{T}
}

Bar{T <: Real} = Union{
Int,
Float64
}

fun_Foo(::Foo) = println("foo!")
fun_Bar(::Bar) = println("bar!")

fun_Foo(0)
fun_Bar(0)
``````
2 Likes

Your parametric methods don’t use the parameter `T`. If you don’t need to use a parameter, you don’t need to write a parametric method e.g. `fun_Bar(::Bar)`; the only reason I can think of for writing an unused parameter is to nudge the compiler to specialize over subtypes of `Type`, `Function`, and `Vararg`, which is only something you should try if you prove that specialization isn’t happening and specializing improves performance without excessively increasing compilation in your program.

If the methods did use the `T`, e.g. `println("foo! ", T)`, then `fun_Foo(0)` would throw an `ERROR: UndefVarError: T not defined` because `Int64` lacks a parameter to assign to `T`. If you use the parameter, then you should make sure the arguments’ concrete types have parameters. Annotating an argument with a parametric union that contains nonparametric types goes against that.

It appears that `Foo{T <: Real} = Union{ Int, Float64, Vector{T}, }` is equivalent to `Foo = Union{ Int, Float64, Vector{T} } where {T <: Real}`; it actually surprised me the first way is legal because I associate the `{T <: Real}` syntax to coming after `where` or to be in the header of `struct` definitions (which is an implicit `where`). So by that logic, `Bar{T <: Real} = Union{ Int, Float64, }` is equivalent to `Bar = Union{Int, Float64} where T`; in this case however, the `where T` is ignored because there’s no T, so `Bar` ends up as a nonparametric `Union` while `Foo` ends up as a parametric `UnionAll`.

Your parametric methods don’t use the parameter `T` . If you don’t need to use a parameter, you don’t need to write a parametric method e.g. `fun_Bar(::Bar)` ; the only reason I can think of for writing an unused parameter is to nudge the compiler to specialize over subtypes of `Type` , `Function` , and `Vararg` , which is only something you should try if you prove that specialization isn’t happening and specializing improves performance without excessively increasing compilation in your program.

I agree that the method doesn’t need to use the parameter in the example, and I was just trying to illustrate what seemed like the main problem in a minimal way. In the actual use case I have additional arguments that are also parametric, and I want everything that is of parametric type to be consistent.

Maybe it is helpful to explain the actual use case. I want to write a `MathOptInterface` wrapper for some solver, where the solver works for different data types as long as all data is of a common type. So I define my optimizer this way:

``````mutable struct Optimizer{T} <: MOI.AbstractOptimizer
<whatever>
end
``````

Now I want to declare which constraint types I can support. Some of those types are non-parametric (`MOI.Zeros` for example) but others have data attached and so are parametric (`MOI.PowerCone{T}` for example). Now I want to collect all of the types I support in a Union:

``````const MySupportedCones{T <: Real} = Union{
MOI.Zeros,
MOI.PowerCone{T}   #<--- removal causes errors
}
``````

If I have `MOI.PowerCone{T}` in this Union, I get the `Foo` situation above. If I comment it out, I get the `Bar` situation. At the moment it is out because the solver does not yet support that type, but I want to leave open the possibility that it will in some future version.

This matters because now I want to declare support for these cones, but I want (in the case of `MOI.PowerCone{T}`) for the type to match that of the Optimizer. So I do something like this:

``````MOI.supports_constraint(
::Optimizer{T},
::Type{<:MOI.VectorOfVariables},
::Type{<:MySupportedCones{T}}
) where {T} = true
``````

This fails if I don’t include `MOI.PowerCone{T}` in the union, because then the `T` is ignored in the union definition. However, I can’t just put `Type{<:MySupportedCones}` as an argument in `supports_constraint` because I want to return `false` if I get a `MOI.PowerCone{Float64}` but the optimizer is an `Optimizer{Float32}` or whatever.

As I said in the first message, I can get around this by just declaring a fake type parametric on `T` to include in the Union. It’s harmless because I know that MOI will never ask me about it anyway, so the question is mostly trying to understand what is going on.

The actual use case just has the same ignored-`where T` issue as the simplified FooBar example, though it does help us see that the `T` exists to constrain both `Optimizer{T}` and `MySupportedCones{T}`, and that the `T is not defined` error isn’t an issue because you have `Optimizer{T}` to specify it. To relate it back to the FooBar example, `fun_Foo(::Foo{T}, ::Vector{T}) where {T} = println("foo! ", T)` would’ve worked for `fun_Foo(0, [0.0])`.

I don’t know why this happens, but I do have a guess. Instead of a parametric method, consider multimethods over different concrete parameters. The call `f(  )` can pick between 2 methods `f(::Foo{Int})` and a `f(::Foo{Float64})`. `f(0)` cannot because `Int` is shared between both sets of types, so you have to resolve the ambiguity with a `f(::Int)` method. However, if we let `Bar` be a `UnionAll`, the complete overlap of `Bar{Int}` and `Bar{Float64}` means the methods `g(::Bar{Int})` and `g(::Bar{Float64})` will always throw an ambiguity error, so it makes sense to force you to write `g(::Bar)` by forcing `Bar` to be a `Union`.

PS: Make sure your method has `where {T<:Real}` to match your cones. Even if you specified `MySupportedCones{T <: Real}`, the method’s `where` segment overrides the UnionAll’s. Example:

``````X{T <: Real} = Vector{T}
# equivalent to const X = Vector{T} where T<:Real
# X{Int} works, X{String} does not

f(::X{T}) where T<:AbstractString = T
# f(["hi"]) works, f() does not

Y{T<:AbstractString} = X{T}
# also overrides to Vector{T} where T<:AbstractString
``````

Thanks - I think I see the problem and the explanation makes sense. As a stopgap I will go with the fake parametric type as a placeholder since that seems to work and it’s only temporary anyway.

Yeah I had considered a couple harebrained schemes to get around it but turns out you really need to use the `T` to get a `UnionAll`. Only real suggestion I have is to use `abstract type _Placeholder{T<:Real} end` and never subtype it, so there’s no default constructor that could make some instance `<: _Placeholder`, whereas `struct MyFakeType{T<:Real} end` is given a constructor that can make instances.