How to interpret "Type{T} where T" in Julias Type-system

I have some problems parsing the Julias type-system… and I came up with a few questions.

  1. Docs - Types: “Type{T} is an abstract type whose only instance is the object T

How can an abstract type have instances??

  1. As far as I can understand is Type{T} kind of a special thing that has the following defining property: " isa(A,Type{B}) is true iff A and B are the same object and that object is a type". Why has this special thing DataType, Union etc. as subtypes?
julia> subtypes(Type)
4-element Array{Any,1}:
 Core.TypeofBottom
 DataType
 Union
 UnionAll
  1. Are all types of some type that is listed above?

  2. Let D be an instance of DataType, thus isa(D, DataType) is true. By the definition of Type{T} is also isa(D,Type{D}) true. This leads me to the conclusion that either DataType <: Type{D} or Type{D} <: DataType must be true. And indeed for D=Int holds the second relation true. But why runnig supertype(Type{Int}) returns Any even though the documentation says “declared types ( DataType ) have unambiguous supertypes” (typeof(Type{Int}) is equal DataType)… Other thing is that isabstracttype(DataType) returns false but Type{Int} <: DataType is true. Is it possible to use non abstract types as supertypes?

I am very confused… if someone knows any other sources that explain this matter I would be grateful. Thanks in advance.

3 Likes

You misunderstood: T can be any type (including an abstract type), while Type{T} is another type, for which the only instance is T. Types can be values, too.

Types (as values) also have a type.

Yes.

No, concrete types cannot have subtypes.

I think the docs are the best source. I would suggest that you think of Type{T} as something which helps you dispatch on types, eg as in

julia> struct Foo end

julia> f(::Foo) = "the value"
f (generic function with 1 method)

julia> f(::Type{Foo}) = "the type"
f (generic function with 2 methods)

julia> f(Foo())
"the value"

julia> f(Foo)
"the type"

and otherwise not worry about the types of types unless you are working on the internals of Julia.

5 Likes

Thank you for your answer.

Yes I do understand but Type{T} where T is an abstract type. (isabstracttype(Type{T} where T) is true)

Yes but I don’t get it why should they be a subtype of Type{T} where T if Type{T} where T is something which introduces some special behaviour on its own.

Sadly, it doesn’t seem to be consistent with the behaviour I described. isconcretetype(DataType) returns true

I just wanted to fully understand it but I think you are right, from the user perspective it shouldn’t be a big deal if I don’t have the whole picture. Thanks

Note that Type{T} where T is an UnionAll type, and as such it is of course not concrete. There is nothing specific to Type here. Cf

julia> isconcretetype(Vector{Int})
true

julia> isconcretetype(Vector{T} where T) # == Vector
false

I don’t see the inconsistency (in the language, I am still not sure what behavior you are describing).

1 Like

The behaviour is the following:
1.

julia> isconcretetype(DataType)
true

julia> Type{Int} <: DataType
true

So Type{Int} is a child of a concrete type.

and is still able to have instances ??

julia> isa(Int, Type{Int})
true

EDIT: maybe the meaning of an abstract type is just that it cannot be instantiated but may somehow have instances

julia> supertype(Type{Int})
Any

julia> Type{Int} <: DataType
true

Supertype returns Any although it is a subtype of DataType

Type{Int} <: DataType is true in the same way that 5<:Int. It’s not saying “is a subtype of”, it’s saying, “is an instance of”.

That is not the case. The following comes straight from REPL help.

<:(T1, T2)


  Subtype operator: returns true if and only if all
  values of type T1 are also of type T2.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> Float64 <: AbstractFloat
  true

  julia> Vector{Int} <: AbstractArray
  true

  julia> Matrix{Float64} <: Matrix{AbstractFloat}
  false

Note that x isa A and A <: B means that x isa B, eg

julia> map(T -> 5 isa T, (Int, Real, Number))
(true, true, true)

The manual tells you what an abstract type is:

Abstract types cannot be instantiated, and serve only as nodes in the type graph, thereby describing sets of related concrete types: those concrete types which are their descendants.

Again, I would recommend that you read the manual and allow some time to digest it. Figuring out these things by trial and error may be a somewhat confusing approach. YMMV.

5<:Int is not true. It’s an error, since 5 is not a type. Type{Int} <: DataType is true because Type{Int} is a subtype of DataType.
I think Type is the only case in the language where an abstract type is a subtype of a concrete type. I also found it surprising and confusing when I first encountered it, though I suppose it doesn’t technically contradict anything the docs say.

That’s not true, although the docs seem to suggest that, and I think that’s the main confusing point here.
One example of a concrete type having a subtype is Type{Int} <: DataType, as @samuel noted. Other examples are Union{} <: Int and Tuple{Int,Float64} <: Tuple{Integer,Real}.

EDIT: I also share @samuel’s confusion about the fact that supertype(Type{Int}) !== DataType. Seems like a bug, as far as I understand, but maybe not, since the docs don’t seems to define what “the supertype” means (the docs also mention the terms “unambiguous supertype”, “immediate supertype”, and “direct supertype”, which persumably refer to the same thing supertype is supposed to return).

I prefer to think of the type system the following way:

  1. A tree of abstract types, with concrete types as the leaf nodes. The root is Any. Relevant keywords are abstract type and struct.
  2. Each node in the above tree describes a UnionAll set when parametrized, invariance is the key concept here. The relevant keyword is where.
  3. Tuples, which are in contrast covariant.

Technically DataType and Type{T} are types, and thus <: can be used to compare them. But for most practical code, I don’t think it is advantageous to think of these types as being part of the type hierarchy. Similarly, Base.Bottom / Union{} is there to make a lattice, and is useful for some code manipulating types, but most users can safely ignore it when encountering the Julia type system for the first time. Keep in mind that its main purpose is organizing dispatch and efficient compilation.

1 Like