Lecture 9

Contents:

  • More about homogeneous composition (hcomp)
  • Cubical univalence (Glue types)
  • The structure identity principle (SIP)
{-# OPTIONS --cubical #-}

module Lecture9-notes where

open import cubical-prelude
open import Lecture7-notes
open import Lecture8-notes hiding (compPath)

private
  variable
    A B : Type 

More about homogeneous composition (hcomp)

Recall from Lecture 8: in order to compose two paths we write:

compPath : {x y z : A}  x  y  y  z  x  z
compPath {x = x} p q i = hcomp  j  λ { (i = i0)  x
                                        ; (i = i1)  q j })
                               (p i)

This is best understood with the following drawing:

    x             z
    ^             ^
    ¦             ¦
  x ¦             ¦ q j
    ¦             ¦
    x ----------> y
          p i

In the drawing the direction i goes left-to-right and j goes bottom-to-top. As we are constructing a path from x to z along i we have i : I in the context already and we put p i as bottom. The direction j that we are doing the composition in is abstracted in the first argument to hcomp.

As we can see here the hcomp operation has a very natural geometric motivation. The following YouTube video might be very helpful to clarify this: https://www.youtube.com/watch?v=MVtlD22Y8SQ

Side remark: this operation is related to lifting conditions for Kan cubical sets, i.e. it’s a form of open box filling analogous to horn filling in Kan complexes.

A more natural form of composition of paths in Cubical Agda is ternary composition:

         x             w
         ^             ^
         ¦             ¦
 p (~ j) ¦             ¦ r j
         ¦             ¦
         y ----------> z
               q i

This is written p ∙∙ q ∙∙ r in the agda/cubical library and implemented by:

_∙∙_∙∙_ : {x y z w : A}  x  y  y  z  z  w  x  w
(p ∙∙ q ∙∙ r) i = hcomp  j  λ { (i = i0)  p (~ j)
                                 ; (i = i1)  r j })
                        (q i)

Using this we can define compPath much slicker:

_∙_ : {x y z : A} → x ≡ y → y ≡ z → x ≡ z
p ∙ q = refl ∙∙ p ∙∙ q

In order to understand the second argument to hcomp we need to talk about partial elements and cubical subtypes. These allow us to write partially specified n-dimensional cubes (i.e. cubes where some faces are missing as in the input to hcomp).

Partial elements

Given an element of the interval r : I there is a predicate IsOne which represents the constraint r = i1. This comes with a proof that i1 is in fact equal to i1 called 1=1 : IsOne i1. We use Greek letters like φ or ψ when such an r should be thought of as being in the domain of IsOne.

Using this we introduce a type of partial elements called Partial φ A, this is a special version of IsOne φ → A with a more extensional judgmental equality (two elements of Partial φ A are considered equal if they represent the same subcube, so the faces of the cubes can for example be given in different order and the two elements will still be considered the same). The idea is that Partial φ A is the type of cubes in A that are only defined when IsOne φ. There is also a dependent version of this called PartialP φ A which allows A to be defined only when IsOne φ.

Partial : ∀ {ℓ} → I → Set ℓ → SSet ℓ

PartialP : ∀ {ℓ} → (φ : I) → Partial φ (Set ℓ) → SSet ℓ

Here SSet is the universe of strict sets.

Cubical Agda has a new form of pattern matching that can be used to introduce partial elements:

partialBool : (i : I)  Partial (i  ~ i) Bool
partialBool i (i = i0) = true
partialBool i (i = i1) = false

The term partialBool i should be thought of a boolean with different values when (i = i0) and (i = i1). Note that this is different from pattern matching on the interval which is not allowed, so we couldn’t have written:

partialBool : (i : I) → Bool
partialBool i0 = true
partialBool i1 = false

Terms of type Partial φ A can also be introduced using a pattern matching lambda and this is what we have been using when we wrote hcomp’s above.

partialBool' : (i : I)  Partial (i  ~ i) Bool
partialBool' i = λ { (i = i0)  true
                   ; (i = i1)  false }

When the cases overlap they must agree (note that the order of the cases doesn’t have to match the interval formula exactly):

partialBool'' : (i j : I)  Partial (~ i  i  (i  j)) Bool
partialBool'' i j = λ { (i = i1)           true
                      ; (i = i1) (j = i1)  true
                      ; (i = i0)           false }

Furthermore IsOne i0 is actually absurd.

_ : {A : Type}  Partial i0 A
_ = λ { () }

With this cubical infrastructure we can now describe the type of the hcomp operation.

hcomp : {A : Type ℓ} {φ : I} (u : I → Partial φ A) (u0 : A) → A

When calling hcomp {φ = φ} u u0 Agda makes sure that u0 agrees with u i0 on φ. The result is then an element of A which agrees with u i1 on φ. The idea being that u0 is the base and u specifies the sides of an open box while hcomp u u0 is the lid of the box. In fact, we can use yet another cubical construction to specify these side conditions in the type of hcomp. For this we need to talk about cubical subtypes.

Cubical subtypes and filling

Cubical Agda also has cubical subtypes as in the CCHM type theory:

_[_↦_] : (A : Type ℓ) (φ : I) (u : Partial φ A) → SSet ℓ
A [ φ ↦ u ] = Sub A φ u

A term v : A [ φ ↦ u ] should be thought of as a term of type A which is definitionally equal to u : A when IsOne φ is satisfied. Any term u : A can be seen as a term of A [ φ ↦ u ] which agrees with itself on φ:

inS : {A : Type ℓ} {φ : I} (u : A) → A [ φ ↦ (λ _ → u) ]

One can also forget that a partial element agrees with u on φ:

outS : {A : Type ℓ} {φ : I} {u : Partial φ A} → A [ φ ↦ u ] → A

They satisfy the following equalities:

outS (inS a) ≐ a

inS {φ = φ} (outS {φ = φ} a) ≐ a

outS {φ = i1} {u} _ ≐ u 1=1

Note that given a : A [ φ ↦ u ] and α : IsOne φ, it is not the case that outS a ≐ u α; however, underneath the pattern binding (φ = i1), one has outS a ≐ u 1=1.

With this we can now give hcomp the following more specific type:

hcomp' : {A : Type } {φ : I} (u : I  Partial φ A) (u0 : A [ φ  u i0 ])  A [ φ  u i1 ]
hcomp' u u0 = inS (hcomp u (outS u0))

This more specific type is of course more informative, but it quickly gets quite annoying to write inS/outS everywhere. So the builtin hcomp operation comes with the slightly less informative type and the side conditions are then implicit and checked internally.

Another very useful operation is open box filling. This produces an element corresponding to the interior of an open box:

hfill' : {A : Type }
         {φ : I}
         (u :  i  Partial φ A)
         (u0 : A [ φ  u i0 ])
         (i : I) 
         A [ φ  ~ i  i 
             { (φ = i1)  u i 1=1
               ; (i = i0)  outS u0
               ; (i = i1)  hcomp u (outS u0) }) ]
hfill' {φ = φ} u u0 i = inS (hcomp  j  λ { (φ = i1)  u (i  j) 1=1
                                            ; (i = i0)  outS u0 })
                                   (outS u0))

This has a slightly less informative type in the cubical-prelude:

hfill : {A : Type ℓ}
        {φ : I}
        (u : ∀ i → Partial φ A)
        (u0 : A [ φ ↦ u i0 ])
        (i : I) →
        A
hfill {φ = φ} u u0 i =
  hcomp (λ j → λ { (φ = i1) → u (i ∧ j) 1=1
                 ; (i = i0) → outS u0 })
        (outS u0)

Having defined this operation we can prove various groupoid laws for _≡_ very cubically as _∙_ is defined using hcomp.

compPathRefl : {A : Type } {x y : A} (p : x  y)  p  refl  p
compPathRefl {x = x} {y = y} p j i = hfill  _  λ { (i = i0)  x
                                                    ; (i = i1)  y })
                                           (inS (p i))
                                           (~ j)

For more examples see Cubical.Foundations.GroupoidLaws in agda/cubical.

Cubical univalence (Glue types) + SIP

A key concept in HoTT/UF is univalence. As we have seen earlier in the course this is assumed as an axiom in Book HoTT. In Cubical Agda it is instead provable and has computational content. This means that transporting with paths constructed using univalence reduces as opposed to Book HoTT where they would be stuck. This simplifies many proofs and make it possible to actually do concrete computations using univalence.

The part of univalence which is most useful for our applications is to be able to turn equivalences (written _≃_ and defined as a Σ-type of a function and a proof that it has contractible fibers) into paths:

ua : {A B : Type }  A  B  A  B
ua {A = A} {B = B} e i = Glue B  { (i = i0)  (A , e)
                                   ; (i = i1)  (B , idEquiv B) })

This term is defined using “Glue types” which were introduced in the CCHM paper. We won’t have time to go into too much details about them today, but for practical applications one can usually forget about them and use ua as a black box. The key idea though is that they are similar to hcomp’s, but instead of attaching (higher dimensional) paths to a base we attach equivalences to a type. One of the original applications of Glue types was to give computational meaning to transporting in a line of types constructed by hcomp in the universe Type.

For us today the important point is that transporting along the path constructed using ua applies the function underlying the equivalence. This is easily proved using transportRefl:

uaβ : (e : A  B) (x : A)  transport (ua e) x  pr₁ e x
uaβ e x = transportRefl (equivFun e x)

Note that for concrete types this typically holds definitionally, but for an arbitrary equivalence e between abstract types A and B we have to prove it.

uaβℕ : (e :   ) (x : )  transport (ua e) x  pr₁ e x
uaβℕ e x = refl

The fact that we have both ua and uaβ suffices to be able to prove the standard formulation of univalence. For details see Cubical.Foundations.Univalence in the agda/cubical library.

The standard way of constructing equivalences is to start with an isomorphism and then improve it into an equivalence. The lemma in the agda/cubical library that does this is

isoToEquiv : {A B : Type ℓ} → Iso A B → A ≃ B

Composing this with ua lets us directly turn isomorphisms into paths:

isoToPath : {A B : Type ℓ} → Iso A B → A ≡ B
isoToPath e = ua (isoToEquiv e)

Time for an example!

not : Bool  Bool
not false = true
not true  = false

notPath : Bool  Bool
notPath = isoToPath (iso not not rem rem)
  where
  rem : (b : Bool)  not (not b)  b
  rem false = refl
  rem true  = refl

_ : transport notPath true  false
_ = refl

Another example, integers:

_ : transport sucPath (pos 0)  pos 1
_ = refl

_ : transport (sucPath  sucPath) (pos 0)  pos 2
_ = refl

_ : transport (sym sucPath) (pos 0)  negsuc 0
_ = refl

Note that we have already used this in the winding function in Lecture 7.

One can do more fun things with sucPath. For example by composing it with itself n times and then transporting we get a new addition function _+ℤ'_. It is direct to prove isEquiv (λ n → n +ℤ' m) as _+ℤ'_ is defined by transport.

addPath :     
addPath zero = refl
addPath (suc n) = addPath n  sucPath

predPath :   
predPath = isoToPath (iso predℤ sucℤ predSuc sucPred)

subPath :     
subPath zero = refl
subPath (suc n) = subPath n  predPath

_+ℤ'_ :     
m +ℤ' pos n    = transport (addPath n) m
m +ℤ' negsuc n = transport (subPath (suc n)) m

-- This agrees with regular addition defined by pattern-matching
+ℤ'≡+ℤ : _+ℤ'_  _+ℤ_
+ℤ'≡+ℤ i m (pos zero) = m
+ℤ'≡+ℤ i m (pos (suc n)) = sucℤ (+ℤ'≡+ℤ i m (pos n))
+ℤ'≡+ℤ i m (negsuc zero) = predℤ m
+ℤ'≡+ℤ i m (negsuc (suc n)) = predℤ (+ℤ'≡+ℤ i m (negsuc n))

-- We can prove that transport is an equivalence easily
isEquivTransport : {A B : Type } (p : A  B)  isEquiv (transport p)
isEquivTransport p =
  transport  i  isEquiv (transp  j  p (i  j)) (~ i))) (idEquiv _ .pr₂)

-- So +ℤ' with a fixed element is an equivalence
isEquivAddℤ' : (m : )  isEquiv  n  n +ℤ' m)
isEquivAddℤ' (pos n)    = isEquivTransport (addPath n)
isEquivAddℤ' (negsuc n) = isEquivTransport (subPath (suc n))

isEquivAddℤ : (m : )  isEquiv  n  n +ℤ m)
isEquivAddℤ = subst  add  (m : )  isEquiv  n  add n m)) +ℤ'≡+ℤ isEquivAddℤ'

The structure identity principle

Combining subst and ua lets us transport any structure on A to get a structure on an equivalent type B:

substEquiv : (S : Type  Type) (e : A  B)  S A  S B
substEquiv S e = subst S (ua e)

In fact this induces an equivalence:

substEquiv≃ : (S : Type  Type) (e : A  B)  S A  S B
substEquiv≃ S e = (substEquiv S e) , (isEquivTransport (ap S (ua e)))

What this says is that any structure on types must be invariant under equivalence. We can for example take IsMonoid for S and get that any two monoid structures on equivalent types A and B are themselves equivalent. This is a simple version of the structure identity principle (SIP). There are various more high powered and generalized versions which also work for structured types (e.g. monoids, groups, etc.) together with their corresponding notions of structured equivalences (e.g. monoid and group isomorphism, etc.). We will look more at this later, but for now let’s look at a nice example where we can use substEquiv to transport functions and their properties between equivalent types.

When programming and proving properties of programs there are usually various tradeoffs between different data representations. Very often one version is suitable for proofs while the other is more suitable for efficient programming. The standard example of them is unary and binary numbers. One way to define binary numbers in Agda is as:

data Pos : Type where
  pos1  : Pos
  x0    : Pos → Pos
  x1    : Pos → Pos

data Bin : Type where
  bin0    : Bin
  binPos  : Pos → Bin

With some work one can prove that this is equivalent to unary numbers (see the cubical-prelude for details):

ℕ≃Bin :   Bin
ℕ≃Bin  = isoToEquiv (iso ℕ→Bin Bin→ℕ Bin→ℕ→Bin ℕ→Bin→ℕ)

We can now use substEquiv to transport addition on together with the fact that it’s associative over to Bin:

SemiGroup : Type  Type
SemiGroup X = Σ _+_  (X  X  X) , ((x y z : X)  x + (y + z)  (x + y) + z)

SemiGroupBin : SemiGroup Bin
SemiGroupBin = substEquiv SemiGroup ℕ≃Bin (_+_ , +-assoc)

_+Bin_ : Bin  Bin  Bin
_+Bin_  = pr₁ SemiGroupBin

+Bin-assoc : (m n o : Bin)  m +Bin (n +Bin o)  (m +Bin n) +Bin o
+Bin-assoc = pr₂ SemiGroupBin

This is nice as it helps us avoid having to repeat work on Bin that we have already done on . This is however not always what we want as _+Bin_ is not very efficient as an addition function on binary numbers. In fact, it will translate its input to unary numbers, add using unary addition, and then translate back. This is of course very naive and what we instead want to do is to use efficient addition on binary numbers, but get the associativity proof for free.

This can be achieved by first defining our fast addition _+B_ : Bin → Bin → Bin and then prove that the map ℕ→Bin : ℕ → Bin maps x + y : ℕ to ℕ→Bin x +B ℕ→Bin y : Bin.

mutual
  _+P_ : Pos  Pos  Pos
  pos1  +P y     = sucPos y
  x0 x  +P pos1  = x1 x
  x0 x  +P x0 y  = x0 (x +P y)
  x0 x  +P x1 y  = x1 (x +P y)
  x1 x  +P pos1  = x0 (sucPos x)
  x1 x  +P x0 y  = x1 (x +P y)
  x1 x  +P x1 y  = x0 (x +PC y)

  _+B_ : Bin  Bin  Bin
  bin0      +B y         = y
  x         +B bin0      = x
  binPos x  +B binPos y  = binPos (x +P y)

  -- Add with carry
  _+PC_ : Pos  Pos  Pos
  pos1  +PC pos1  = x1 pos1
  pos1  +PC x0 y  = x0 (sucPos y)
  pos1  +PC x1 y  = x1 (sucPos y)
  x0 x  +PC pos1  = x0 (sucPos x)
  x0 x  +PC x0 y  = x1 (x +P y)
  x0 x  +PC x1 y  = x0 (x +PC y)
  x1 x  +PC pos1  = x1 (sucPos x)
  x1 x  +PC x0 y  = x0 (x +PC y)
  x1 x  +PC x1 y  = x1 (x +PC y)

-- Correctness:
+PC-suc : (x y : Pos)  x +PC y  sucPos (x +P y)
+PC-suc pos1 pos1     = refl
+PC-suc pos1 (x0 y)   = refl
+PC-suc pos1 (x1 y)   = refl
+PC-suc (x0 x) pos1   = refl
+PC-suc (x0 x) (x0 y) = refl
+PC-suc (x0 x) (x1 y) = ap x0 (+PC-suc x y)
+PC-suc (x1 x) pos1   = refl
+PC-suc (x1 x) (x0 y) = ap x0 (+PC-suc x y)
+PC-suc (x1 x) (x1 y) = refl

sucPos-+P : (x y : Pos)  sucPos (x +P y)  sucPos x +P y
sucPos-+P pos1 pos1     = refl
sucPos-+P pos1 (x0 y)   = refl
sucPos-+P pos1 (x1 y)   = refl
sucPos-+P (x0 x) pos1   = refl
sucPos-+P (x0 x) (x0 y) = refl
sucPos-+P (x0 x) (x1 y) = ap x0 (sym (+PC-suc x y))
sucPos-+P (x1 x) pos1   = refl
sucPos-+P (x1 x) (x0 y) = ap x0 (sucPos-+P x y)
sucPos-+P (x1 x) (x1 y) = ap x1 (+PC-suc  x y  sucPos-+P x y)

ℕ→Pos-+P : (x y : )  ℕ→Pos (suc x + suc y)  ℕ→Pos (suc x) +P ℕ→Pos (suc y)
ℕ→Pos-+P zero _    = refl
ℕ→Pos-+P (suc x) y = ap sucPos (ℕ→Pos-+P x y)  sucPos-+P (ℕ→Pos (suc x)) (ℕ→Pos (suc y))

ℕ→Bin-+B : (x y : )  ℕ→Bin (x + y)  ℕ→Bin x +B ℕ→Bin y
ℕ→Bin-+B zero y          = refl
ℕ→Bin-+B (suc x) zero    = ap  x  binPos (ℕ→Pos (suc x))) (+-zero x)
ℕ→Bin-+B (suc x) (suc y) = ap binPos (ℕ→Pos-+P x y)

Having done this it’s now straightforward to prove that _+B_ is associative using the fact that _+Bin_ is:

+B≡+Bin : _+B_  _+Bin_
+B≡+Bin i x y = goal x y i
  where
  goal : (x y : Bin)  x +B y  ℕ→Bin (Bin→ℕ x + Bin→ℕ y)
  goal x y =   i  Bin→ℕ→Bin x (~ i) +B Bin→ℕ→Bin y (~ i))
             sym (ℕ→Bin-+B (Bin→ℕ x) (Bin→ℕ y))

+B-assoc : (m n o : Bin)  m +B (n +B o)  (m +B n) +B o
+B-assoc m n o =
            i  +B≡+Bin i m (+B≡+Bin i n o))
               ∙∙ +Bin-assoc m n o
               ∙∙  i  +B≡+Bin (~ i) (+B≡+Bin (~ i) m n) o)

This kind of proofs quickly get tedious and it turns out we can streamline them using a more high prowered version of the SIP. Indeed, what we really want to do is to characterize the equality of T-structured types, i.e. we want a proof that two types equipped with T-structures are equal if there is a T-structure preserving equivalence between them. In the semigroup example above T would just be the structure of a magma (i.e. a type with a binary operation) together with magma preserving equivalence so that we can transport for example the fact that the magma is a semigroup over to the other magma.

We formalize this and make it more convenient to use using a lot of automation in the agda/cubical library. This is documented with various more examples in:

Internalizing Representation Independence with Univalence Carlo Angiuli, Evan Cavallo, Anders Mörtberg, Max Zeuner https://dl.acm.org/doi/10.1145/3434293

The binary numbers example with efficient addition is spelled out in detail in Section 2.1.1 of:

https://www.doi.org/10.1017/S0956796821000034 (Can be downloaded from: https://staff.math.su.se/anders.mortberg/papers/cubicalagda2.pdf)