Lecture 7: Cubical Agda

Contents:

  • The interval and Path/PathP types
  • Cubical higher inductive types

Some introductory pointers for further reading

Documentation of the Cubical Agda mode can be found here.

These lectures about Cubical Agda will be inspired by my material from the 2020 EPIT school on HoTT.

For students interested in a more in depth introduction to cubical type theory see my lecture notes for the 2019 HoTT school. Those notes contain a lot more background and motivation to cubical methods in HoTT and an extensive list of references for those that want to read more background material.

If one wants a library to work with when doing Cubical Agda there is the agda/cubical library that I started developing with Andrea Vezzosi (the implementor of Cubical Agda) in 2018 and which has now over 70 contributors. It contains a variety of things, including data structures, algebra, synthetic homotopy and cohomology theory, etc..

For a slower-paced introduction to mathematics in Cubical Agda, there is the more recent 1lab. Although meant to be read on the web, it also doubles as a community-maintained library of formalized mathematics, most notably category theory.

Cubical Agda

To make Agda cubical simply add the following option:

{-# OPTIONS --cubical #-}

module Lecture7-notes where

We also have a small cubical prelude which sets things up to work nicely and which provides whatever we might need for the lectures.

open import cubical-prelude

The key idea in cubical type theories like Cubical Agda is to not have equality be inductively defined as in Book HoTT, but rather we assume that there is a primitive interval and define equality literally as paths, i.e. as functions out of the interval. By iterating these paths we get squares, cubes, hypercubes, …, making the type theory inherently cubical.

The interval and path types

The interval is a primitive concept in Cubical Agda. It’s written I. It has two endpoints:

  i0 : I
  i1 : I

These stand for “interval 0” and “interval 1”.

We can apply a function out of the interval to an endpoint just like we would with any Agda function:

apply0 : (A : Type ) (p : I  A)  A
apply0 A p = p i0

The equality type _≡_ is not inductively defined in Cubical Agda, instead it’s a builtin primitive notion defined using the interval. An element of x ≡ y consists of a function p : I → A such that p i0 is definitionally x and p i1 is definitionally y. The check that the endpoints are correct when we provide a p : I → A is automatically performed by Agda during typechecking, so introducing an element of x ≡ y is done just like how we introduce elements of I → A but Agda will check the side conditions.

We can hence write paths using λ-abstraction:

mypath : {A : Type } (x : A)  x  x
mypath x = λ i  x

As explained above Agda checks that whatever we written as definition matches the path that we have provided (so the endpoints have to be correct). In this case everything is fine and mypath can be thought of as a proof reflexivity. Let’s give it a nicer name and more implicit arguments:

refl : {A : Type } {x : A}  x  x
refl {x = x} = λ i  x

The notation {x = x} lets us access the implicit argument x (the x in the LHS of x = x) and rename it to x (the x in the RHS x = x) in the body of refl. We could just as well have written:

refl : {A : Type ℓ} {x : A} → x ≡ x
refl {x = y} = λ i → y

Note that we cannot pattern-match on interval variables as I is not inductively defined. Try uncommenting and typing C-c C-c in the hole:

oops : {A : Type} → I → A
oops r = {!r!}

It quickly gets tiring to write {A : Type ℓ} everywhere, so let’s assume that we have some types (in fact, we’ve already assumed that is a Level in the cubical-prelude):

private
  variable
    A B : Type 

This will make A and B elements of different universes (all arguments is maximally generalized) and all definitions that use them will have them as implicit arguments.

We can now implement some basic operations on _≡_. Let’s start with ap:

ap : (f : A  B) {x y : A}  x  y  f x  f y
ap f p i = f (p i)

Note that the definition differs from the Book HoTT definition in that it is not defined by path induction or pattern-matching on p, but rather it’s just a direct definition as a composition of functions. Agda treats p : x ≡ y like any function, so we can apply it to i to get an element of A which at i0 is x and at i1 is y. By applying f to this element we hence get an element of B which at i0 is f x and at i1 is f y.

As this is just function composition it satisfies lots of nice definitional equalities, see the exercises. Some of these are not satisfied by the Book HoTT definition of ap.

In Book HoTT function extensionality is proved as a consequence of univalence using a rather ingenious proof due to Voevodsky, but in cubical systems it has a much more direct proof. As paths are just functions we can get it by swapping the arguments to p:

funExt : {f g : A  B} (p : (x : A)  f x  g x)  f  g
funExt p i x = p x i

To see that this has the correct type, note that when i is i0 we have p x i0 = f x and when i is i1 we have p x i1 = g x, so by η for function types we have a path f ≡ g as desired.

The interval has additional operations:

Minimum:     _∧_ : I → I → I             (corresponds to min(i,j))
Maximum:     _∨_ : I → I → I             (corresponds to max(i,j))
Symmetry:     ~_ : I → I                 (corresponds to 1 - i)

These satisfy the equations of a De Morgan algebra (i.e. a distributive lattice ( , , i0 , i1) with an “De Morgan” involution ~). This just means that we have the following kinds of equations definitionally:

i0 ∨ i    ≐ i
i  ∨ i1   ≐ i1
i  ∨ j    ≐ j ∨ i
i0 ∧ i    ≐ i0
i1 ∧ i    ≐ i
i  ∧ j    ≐ j ∧ i
~ (~ i)   ≐ i
i0        ≐ ~ i1
~ (i ∨ j) ≐ ~ i ∧ ~ j
~ (i ∧ j) ≐ ~ i ∨ ~ j

However, we do not have i ∨ ~ i = i1 and i ∧ ~ i = i0. The reason is that I represents an abstract interval, so we if we think of it as the real interval [0,1] ⊂ ℝ we clearly don’t always have “max(i,1-i) = 1” or “min(i,1-i) = 0)” for all i ∈ [0,1].

These operations on I are very useful as they let us define even more things directly. For example symmetry of paths is easily defined using ~_.

sym : {x y : A}  x  y  y  x
sym p i = p (~ i)

Remark: this has been called ⁻¹ and ! in the previous lectures. Here we stick to sym for the cubical version following the agda/cubical notation.

The operations _∧_ and _∨_ are called connections and let us build higher dimensional cubes from lower dimensional ones, for example if we have a path p : x ≡ y then

  sq i j = p (i ∧ j)

is a square (as we’ve parametrized by i and j) with the following boundary:

   sq i0 j = p (i0 ∧ j) = p i0 = x
   sq i1 j = p (i1 ∧ j) = p j
   sq i i0 = p (i ∧ i0) = p i0 = x
   sq i i1 = p (i ∧ i1) = p i

If we draw this we get:

             p
       x --------> y
       ^           ^
       ¦           ¦
  refl ¦     sq    ¦ p
       ¦           ¦
       ¦           ¦
       x --------> x
           refl

Being able to make this square directly is very useful. It for example let’s prove that singletons are contractible (a.k.a. based path induction).

We define the type of singletons as follows

singl : {A : Type } (a : A)  Type 
singl {A = A} a = Σ x  A , a  x

To show that this type is contractible we need to provide a center of contraction and the fact that any element of it is path-equal to the center

isContrSingl : (x : A)  isContr (singl x)
isContrSingl x = ctr , prf
  where
  -- The center is just a pair with x and refl
  ctr : singl x
  ctr = x , refl

  -- We then need to prove that ctr is equal to any element s : singl x.
  -- This is an equality in a Σ-type, so the first component is a path
  -- and the second is a path over the path we pick as first argument,
  -- i.e. the second component is a square. In fact, we need a square
  -- relating refl and pax over a path between refl and pax, so we can
  -- use an _∧_ connection.
  prf : (s : singl x)  ctr  s
  prf (y , pax) i = (pax i) , λ j  pax (i  j)

As we saw in the second component of prf we often need squares when proving things. In fact, pax (i ∧ j) is a path relating refl to pax over another path λ j → x ≡ pax j. This notion of path over a path is very useful when working in Book HoTT as we’ve seen in the previous lectures, this is also the case when working cubically. In Cubical Agda path-overs are a primitive notion called PathP (“Path over a Path”). In general PathP A x y has

   A : I → Type ℓ
   x : A i0
   y : A i1

So PathP lets us natively define heteorgeneous paths, i.e. paths where the endpoints are in different types. This allows us to specify the type of the second component of prf:

prf' : (x : A) (s : singl x)  (x , refl)  s
prf' x (y , pax) i = (pax i) , λ j  goal i j
  where
  goal : PathP  j  x  pax j) refl pax
  goal i j = pax (i  j)

Just like _×_ is a special case of Σ-types we have that _≡_ is a special case of PathP. In fact, x ≡ y is just short for PathP (λ _ → A) x y:

reflP : {x : A}  PathP  _  A) x x
reflP = refl

Working directly with paths and equalities makes many proofs from Book HoTT very short:

isContrΠ : {B : A  Type } (h : (x : A)  isContr (B x))
          isContr ((x : A)  B x)
isContrΠ h =  x  pr₁ (h x)) ,  f i x  pr₂ (h x) (f x) i)

Cubical higher inductive types

We have seen various HITs earlier in the course. These were added axiomatically to Agda by postulating their existence together with suitable elimination/induction principles. In Cubical Agda they are instead added just like any inductive data type, but with path constructors. This is made possible by the fact that paths in Cubical Agda are just fancy functions.

The circle

We can define the circle as the following simple data declaration:

data  : Type₀ where
  base : 
  loop : base  base

We can write functions on using pattern-matching:

double :   
double base = base
double (loop i) = (loop  loop) i

Note that loop takes an i : I argument. This is not very surprising as it’s a path of type base ≡ base, but it’s an important difference to Book HoTT where we instead would have to state the equation using ap. Having the native notion of equality be heterogeneous makes it possible to quite directly define a general schema for a large class of HITs and use it in the implementation of a system like Cubical Agda.

Let’s use univalence to compute some winding numbers on the circle. We first define a family of types over the circle with fibers being the integers.

helix :   Type₀
helix base     = 
helix (loop i) = sucPath i

Here univalence is baked into sucPath : ℤ ≡ ℤ. The loopspace of the circle is then defined as

ΩS¹ : Type₀
ΩS¹ = base  base

and we can then define a function computing how many times we’ve looped around the circle by:

winding : ΩS¹  
winding p = transp  i  helix (p i)) i0 (pos 0)

Here transp is a cubical transport function. We’ll talk about it in more detail in the next lecture, but for now we can observe that it reduces as expected:

_ : winding  i  double ((loop  loop) i))  pos 4
_ = refl

This would not reduce definitionally in Book HoTT as univalence is an axiom. Having things compute definitionally makes it possible to substantially simplify many proofs from Book HoTT in Cubical Agda.

We can in fact prove that winding is an equivalence, this is very similar to the Book HoTT proof and uses the encode-decode method. For details about how this proof looks in Cubical Agda see the Cubical.HITs.S1.Base file in the agda/cubical library.

The torus

We can define the torus as:

data Torus : Type₀ where
  point : Torus
  line1 : point  point
  line2 : point  point
  square : PathP  i  line1 i  line1 i) line2 line2

The square corresponds to the usual folding diagram from topology (where p is short for point):

             line1
        p ----------> p
        ^             ^
        ¦             ¦
  line2 ¦             ¦ line2
        ¦             ¦
        p ----------> p
             line1

Proving that it is equivalent to two circles is pretty much trivial as we have definitional computation rules for all constructors, including higher ones:

t2c : Torus   × 
t2c point        = (base , base)
t2c (line1 i)    = (loop i , base)
t2c (line2 j)    = (base , loop j)
t2c (square i j) = (loop i , loop j)

c2t :  ×   Torus
c2t (base   , base)   = point
c2t (loop i , base)   = line1 i
c2t (base   , loop j) = line2 j
c2t (loop i , loop j) = square i j

c2t-t2c : (t : Torus)  c2t (t2c t)  t
c2t-t2c point        = refl
c2t-t2c (line1 _)    = refl
c2t-t2c (line2 _)    = refl
c2t-t2c (square _ _) = refl

t2c-c2t : (p :  × )  t2c (c2t p)  p
t2c-c2t (base   , base)   = refl
t2c-c2t (base   , loop _) = refl
t2c-c2t (loop _ , base)   = refl
t2c-c2t (loop _ , loop _) = refl

Using univalence we get the following equality:

Torus≡S¹×S¹ : Torus   × 
Torus≡S¹×S¹ = isoToPath (iso t2c c2t t2c-c2t c2t-t2c)

We can also directly compute winding numbers on the torus

windingTorus : point  point   × 
windingTorus l = ( winding  i  pr₁ (t2c (l i)))
                 , winding  i  pr₂ (t2c (l i))))

_ : windingTorus (line1  sym line2)  (pos 1 , negsuc 0)
_ = refl

Bonus content if there is time

Interval

data Interval : Type₀ where
  zero : Interval
  one  : Interval
  seg  : zero  one

Suspension

data Susp (A : Type ) : Type  where
  north : Susp A
  south : Susp A
  merid : (a : A)  north  south

We can define Dan’s Circle2 as the suspension of Bool, or we can do it directly as:

data Circle2 : Type₀ where
  north : Circle2
  south : Circle2
  west  : north  south
  east  : north  south

Pushouts

data Pushout { ℓ' ℓ''} {A : Type } {B : Type ℓ'} {C : Type ℓ''}
             (f : A  B) (g : A  C) : Type (  ℓ'  ℓ'') where
  inl : B  Pushout f g
  inr : C  Pushout f g
  push : (a : A)  inl (f a)  inr (g a)

Relation quotient

data _/ₜ_ { ℓ'} (A : Type ) (R : A  A  Type ℓ') : Type (  ℓ') where
  [_] : (a : A)  A /ₜ R
  eq/ : (a b : A)  (r : R a b)  [ a ]  [ b ]

Propositional truncation

data ∥_∥₋₁ {} (A : Type ) : Type  where
  ∣_∣₋₁ : A   A ∥₋₁
  squash₁ : (x y :  A ∥₋₁)  x  y