Universes

Contents

Types of types

Types also have types!

Pikelet> :t S32
Type

We call this special 'type of types' a universe.

You might then wonder, “what is the type of Type?” That's a good question! Clever people have figured out that if Type was its own type, ie. Type : Type, it would lead situations like Girard's paradox, (the type theory equivalent of the more well-known Russel's paradox in set theory). There is nothing worse than paradoxes in a type system, so instead we create ourselves a new universe called Type^1:

Pikelet> :t Type
Type^1

We keep on going like this, giving us a hierarchy of universes, as many as we need for a given program:

Type : Type^1 : Type^2 : Type^3 : ...

We call the number given to each universe the level of that universe. You can think of these universes as larger and larger collections of things, with the smaller universes being contained within the larger universes:

.- Type^2 -----------------------------------------------------------------------.
|                                                    Array n Type^1              |
|                                                                                |
|  .- Type^1 -----------------------------------------------------------------.  |
|  |                                                                          |  |
|  |  .- Type -------------------------------.                                |  |
|  |  |                Array n String        |                                |  |
|  |  |                                      |                                |  |
|  |  |            Record { x : F32 }        |          Array n Type          |  |
|  |  |                                      |                                |  |
|  |  |  .- S32 --------------.              |                                |  |
|  |  |  | ..., -1, 0, 1, ... |              |       Nat -> Type -> Type      |  |
|  |  |  '--------------------'              |                                |  |
|  |  |                                      |                                |  |
|  |  |  .- Record {} -.   .- Bool -------.  |                                |  |
|  |  |  |  record {}  |   |  true, false |  |          Type -> Type          |  |
|  |  |  '-------------'   '--------------'  |                                |  |
|  |  |                                      |                                |  |
|  |  |  .- String ----------------------.   |  .- Record { t : Type } ----.  |  |
|  |  |  | "hello", "byee!", "hoho", ... |   |  |  record { t = String },  |  |  |
|  |  |  '-------------------------------'   |  |  record { t = U32 }, ... |  |  |
|  |  '--------------------------------------'  '--------------------------'  |  |
|  '--------------------------------------------------------------------------'  |
'--------------------------------------------------------------------------------'

Note that in most regular programming you will rarely see anything above Type, and even more rarely still will you see things above Type^1, so all of this might seem a little excessive. That being said, we believe it is important enough to plug this gap in our type system while we have the chance.

Cumulativity

Because the level of a universe corresponds to some notion of it's 'size', this suggests that larger universes should be able to contain all the other things smaller than themselves. This is reflected in Pikelet too:

Pikelet> Bool : Type        -- ok
Pikelet> Bool : Type^2      -- ok
Pikelet> Type^1 : Type^3    -- ok
Pikelet> Type^3 : Type^1    -- error!

Syntactic sugar

Note that Type is actually just sugar for Type^0:

Pikelet> :t Type^0
Type^1

Shifting universes

Often we'll write definitions in terms of Type, without worrying about the universe levels. For example the identity function can be defined with a type parameter in the universe of Type:

Pikelet> :let id = \(a : Type) (x : a) => x
id : (a : Type) (x : a) -> a

This then allows us to use it with values:

Pikelet> id String "hello"   -- ok
Pikelet> id S32 1            -- ok

Sadly because of our universe hierarchy, we can't use our identity function at the type level!

Pikelet> id Type String                 -- error!
Pikelet> id ((a : Type) -> a -> a) id   -- error!
Pikelet> id Type^1 Type                 -- error!

This would seem like it would be terrible for code reuse - you would need to create a new id function for every universe level! Thankfully we have a simple solution: We allow identifiers to be shifted to the correct universe level using the ^ notation. This shifts the given definition to the desired universe level:

Pikelet> :t id^1
(a : Type^1) -> a -> a

We can then use the shifted identity functions like so:

Pikelet> id^1 Type String                    -- ok
Pikelet> id^1 ((a : Type) -> a -> a) id      -- ok
Pikelet> id^2 Type^1 Type                    -- ok

Field projections can also have shifts applied to them:

Pikelet> prelude.id^1 Type String