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 supertypes 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":
[1] foo(::Int64) in Main at REPL[1]:1
[2] foo(::Type{Int64}) in Main at REPL[2]: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. :smile:

Thank you for your answers!
Oh and did you create those type hierarchy “visualizations” yourself or is there some function to print the type graph?

I get that, but I think it’s strange (or unsatisfying) that if A <: B is true, subtypes(B) does not list A.

I wrote them out by hand. I thought I saw a package once for making Julia type graphs, but I can’t find it now.

Array{T,N} has an infinite number of subtypes, so I think it’s reasonable to not list them all out. :slight_smile:

2 Likes

Fair enough. :smile: