# Parametric and Union Types

Hi Community!

While playing around with types, some questions came up:

What is the difference between `Type{<:Union{Int,String}}` and `Union{Type{Int}, Type{String}}`?

``````Type{<:Union{Int,String}} |> supertype  # Any
Type{<:Union{Int,String}} |> eltype  # Any
Int isa Type{<:Union{Int,String}}  # true
Union{Type{Int}, Type{String}} |> supertype  # ERROR: MethodError
Union{Type{Int}, Type{String}} |> eltype  # Any
Int isa Union{Type{Int}, Type{String}}  # true
Type{<:Union{Int,String}} == Union{Type{Int}, Type{String}}  # false
``````

Are parametric types without their parameters really types? Are they higher kinded types?
Why is `Array{Int} <: Array` true but, `Array` is nowhere to find when iteratively checking `supertype`s of `Array{Int}`?

``````Array{Int} <: Array  # true
Array{Int} |> supertype  # DenseArray{Int64,N} where N
Array{Int} |> supertype |> supertype  # AbstractArray{Int64,N} where N
Array{Int} |> supertype |> supertype |> supertype  # Any
``````

Edit:

Is there a type that represents an `Array` that can only hold `Int` and `String` typed elements?

2 Likes

I’m going to attempt to rescue this thread before it gets completely buried. I might not be able to answer all your questions.

## 2

Parametric types with their parameters unspecified are abstract types. In particular, they are referred to as UnionAll Types. They represent the union of all types that are possible by assigning a concrete type to each type parameter.

`Array` is shorthand for `Array{T,N} where N where T`. Note that you can partially instantiate a parametric type:

``````julia> Array{Int64} == Array{Int64,N} where N
true
``````

A partially instantiated parametric type is still an abstract type:

``````julia> isconcretetype(Array)
false

julia> isconcretetype(Array{Float64})
false

julia> isconcretetype(Array{Float64,1})
true
``````

## 3

Yes, if we restrict ourselves to one-dimensional arrays, then we have the following concrete type:

``````Array{Union{Int,String},1}
``````

If the number of dimensions is left unspecified, then we have an abstract type:

``````julia> IntStringArray{N} = Array{Union{Int, String}, N} where N
Array{Union{Int64, String},N} where N

julia> isconcretetype(IntStringArray)
false

julia> isconcretetype(IntStringArray{2})
true

julia> x = IntStringArray(undef, 0)
0-element Array{Union{Int64, String},1}

julia> push!(x, 4)
1-element Array{Union{Int64, String},1}:
4

julia> push!(x, "hello")
2-element Array{Union{Int64, String},1}:
4
"hello"

julia> push!(x, 1.2)
ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type Union{Int64, String}
``````

## 1

I’m not sure if this is a point of confusion or not, but a type such as `Type{Int64}` is a sort of meta-type that represents the type object `Int64` rather than an instance of `Int64`. For example, we can make two separate `foo` methods as follows:

``````julia> foo(::Int64) = 1
foo (generic function with 1 method)

julia> foo(::Type{Int64}) = 2
foo (generic function with 2 methods)

julia> methods(foo)
# 2 methods for generic function "foo":
 foo(::Int64) in Main at REPL:1
 foo(::Type{Int64}) in Main at REPL:1

julia> foo(100)
1

julia> foo(Int64)
2
``````

Regarding the difference between

`Type{T} where T<:Union{Int, String}`

and

`Union{Type{Int}, Type{String}}`

…They do seem to be functionally equivalent, but Julia doesn’t seem to be able to do the algebra/set operations needed to declare them `==`. I’m not sure precisely what’s going on there.

Edit:

It could be because the first is a `UnionAll` type and the second is a `Union` type:

``````julia> typeof(Type{T} where T<:Union{Int, String})
UnionAll

julia> typeof(Union{Type{Int}, Type{String}})
Union
``````

It looks like Julia doesn’t have a specialized `==` method for comparing `Union` and `UnionAll` types:

``````julia> (Type{T} where T<:Union{Int, String}) == Union{Type{Int}, Type{String}}
false

julia> @which (Type{T} where T<:Union{Int, String}) == Union{Type{Int}, Type{String}}
==(T::Type, S::Type) in Base at operators.jl:162
``````

It might be possible to implement a specialized `==(::Union, ::UnionAll)` method that would catch this sort of scenario.

5 Likes

Thank you, that really helps!

Regarding 2.: So UnionAll types are unions which include all types defined by parametric types, but are not their supertype, correct?

I also wonder why Union and UnionAll are separate types. Maybe because the latter are infinite sets?

They are different beasts, `UnionAll` is for unions over parameters (`... where ...`), eg

``````julia> Vector
Array{T,1} where T

julia> typeof(Vector)
UnionAll
``````

while `Union` is for enumerated types.

I’m not sure if I understand your exact meaning here, but to clarify:

`Union` and `UnionAll` are types of types. (The other common type of type is `DataType`.) So, `Array{T,1} where T` is a type of type `UnionAll`, but arrays of type `Array{T,1} where T` are not a subtype of `UnionAll`:

``````julia> typeof(Array{T,1} where T)
UnionAll

julia> (Array{T,1} where T) <: UnionAll
false
``````

And similarly for `Union` types:

``````julia> typeof(Union{Int64, String})
Union

julia> Union{Int64, String} <: Union
false
``````

In other words, `Array{T,N} where N where T` is a node on the hierarchy of types of arrays, whereas `UnionAll` is a node on the hierarchy of types of types. For example,

``````julia> Array{Int64,1} <: (Array{T,N} where N where T)
true

julia> (Array{T,N} where N where T) <: AbstractArray
true

julia> isconcretetype(UnionAll)
true

julia> supertype(UnionAll)
Type{T}

julia> supertype(supertype(UnionAll))
Any
``````

What still confuses me is the snipped under my original 2:

``````Array{Int} <: Array  # true
Array{Int} |> supertype  # DenseArray{Int64,N} where N
Array{Int} |> supertype |> supertype  # AbstractArray{Int64,N} where N
Array{Int} |> supertype |> supertype |> supertype  # Any
``````

Why is it that `Array{Int}` is a subtype of `Array` (according to `<:`) but `Array` is not a supertype of `Array{Int}`? I was thinking one implied the other.

In fact, `Array` does not even have subtypes:

``````Array |> subtypes  # 0-element Array{Type,1}
``````

This made me think that a parametric type without its parameters specified is not a type. At least not of the same kind as other types.

It is my understanding that `supertype()` and `subtypes()` only return the explicitly declared supertype and subtypes, respectively. `Array{Int,1}` and `Array{String,1}` are not explicitly declared types. They are only declared implicitly through the parametric definition `Array{T,N} where {T,N}`. (Henceforth, I will drop the `where {T,N}` for brevity.) Therefore, `subtypes(Array)` returns an empty array because there are no explicitly declared subtypes of `Array{T,N}`.

The particular branch of the type hierarchy (tree) that we’re looking at is explicitly declared as the following:

``````AbstractArray ⟶ DenseArray ⟶ Array
``````

Or with the type parameters made explicit:

``````AbstractArray{T,N} where {T,N}
⮑ DenseArray{T,N} where {T,N}
⮑ Array{T,N} where {T,N}
``````

Note that `Array{T,N}` is a terminal node in the tree. There are no explicitly declared subtypes of `Array{T,N}`.

So, you could say that `Array{Int,1}` lives on the same node in the hierarchy as `Array{T,N}`. But `Array{Int,1}` is a subtype of `Array{T,N}` because it represents a subset of the various types that are implied by `Array{T,N}`.

Said another way, `Array{T,N}` is a supertype of `Array{Int,1}`, but it’s not returned by `supertype(Array{Int,1})` because it lives on the same node as `Array{Int,1}`.

1 Like

`Array{Int} <: Array && Array >: Array{Int}`, those are symmetric operations.

Yea, I guess it works like that.
It does feel a bit unsatisfying on a formal level, though. I get that, but I think it’s strange (or unsatisfying) that if `A <: B` is `true`, `subtypes(B)` does not list `A`.
`Array{T,N}` has an infinite number of subtypes, so I think it’s reasonable to not list them all out. Fair enough. 