blog/content/posts/2022-09-20-higher-inductive...

5.6 KiB
Raw Blame History

+++ title = "Higher Inductive Types" slug = "higher-inductive-types" date = 2022-09-20 tags = ["type-theory"] toc = true math = true draft = true +++

Higher inductive types (HIT) are central to developing cubical type theory. What this article will try to do is develop an approach to understanding higher inductive types based on my struggles to learn the topic.

Ordinary inductive types

So first off, what is an inductive type? These are a kind of data structure that's commonly used in functional programming. For example, consider the definition of natural numbers (Nat):

data Nat : Set where
  zero : Nat
  suc : Nat  Nat

This defines all Nats as either zero, or one more than another Nat. For example, here's the first few natural numbers and their corresponding representation using this data structure:

0  zero
1  suc zero
2  suc (suc zero)
3  suc (suc (suc zero))
4  suc (suc (suc (suc zero)))
5  suc (suc (suc (suc (suc zero))))

Why is this representation useful? Well, if you remember proof by induction from maybe high school geometry, you'll recall that we can prove things about all natural numbers by simply proving that it's true for the base case 0, and then proving that it's true for any inductive case n, given that the previous case n - 1 is true.

This kind of definition of natural numbers makes this induction structure much more clear. For example, look at the definition of a tree:

data Tree (A : Set) : Set where
  leaf : A  Tree A
  left : Tree A  Tree A
  right : Tree A  Tree A

We can do induction on trees by simply proving it's true for (1) the base case, (2) the left case, and (3) the right case. In fact, all inductive data structures have this kind of induction principle. So say you wanted to prove that 1 + 2 + 3 + \cdots + n = \frac{n\cdot(n+1)}{2} for all $n \in \mathbb{N}$, then you could say:

(click here for boring requisites)
open import Relation.Binary.PropositionalEquality using (_≡_; refl; cong; sym; module ≡-Reasoning)
open ≡-Reasoning using (begin_; _≡⟨_⟩_; _≡⟨⟩_; step-≡; _∎)
open import Data.Product using (_×_)
open import Data.Nat using (; zero; suc; _+_; _*_)
open import Data.Nat.DivMod using (_/_; 0/n≡0; n/n≡1; m*n/n≡m)
open import Data.Nat.Properties using (+-assoc; *-identityˡ; *-comm; *-distribʳ-+; +-comm)

sum-to-n :   
sum-to-n zero = zero
sum-to-n (suc x) = (suc x) + (sum-to-n x)

distrib-/ :  (a b c : )  a / c + b / c  (a + b) / c
distrib-/ zero b c =
  begin
    zero / c + b / c
  ≡⟨ cong (_+ b / c) (0/n≡0 c) 
    b / c
  ≡⟨ cong (_/ c) refl 
    (zero + b) / c
  
distrib-/ (suc a) b c =
  begin
    (1 + a) / c + b / c
  ≡⟨ cong (_+ b / c) (sym (distrib-/ 1 a c)) 
    1 / c + a / c + b / c
  ≡⟨ +-assoc (1 / c) (a / c) (b / c) 
    1 / c + (a / c + b / c)
  ≡⟨ cong (1 / c +_) (distrib-/ a b c) 
    1 / c + (a + b) / c
  ≡⟨ distrib-/ 1 (a + b) c 
    (suc a + b) / c
  
-- Here's the proposition we want to prove:
our-prop :  (n : )  sum-to-n n  n * (n + 1) / 2

-- How do we prove this?
-- Well, we know it's true for zero:
base-case : sum-to-n 0  0 * (0 + 1) / 2
base-case = refl

-- The next part is proving that it's true for any n + 1, given that it's true
-- for the previous case n:
inductive-case :  {n : }
   (inductive-hypothesis : sum-to-n n  n * (n + 1) / 2)
   sum-to-n (suc n)  (suc n) * (suc n + 1) / 2
Inductive case proof, expand if you're interested
inductive-case {n} p =
  begin
    sum-to-n (suc n)
  ≡⟨⟩ -- Expanding definition of sum-to-n
    suc n + sum-to-n n
  ≡⟨ cong (suc n +_) p  -- Substituting the previous case
    suc n + n * (n + 1) / 2
  ≡⟨ cong (_+ n * (n + 1) / 2) (sym (m*n/n≡m (suc n) 2)) 
    (suc n * 2) / 2 + (n * (n + 1)) / 2
  ≡⟨ distrib-/ (suc n * 2) (n * (n + 1)) 2 
    (suc n * 2 + n * (n + 1)) / 2
  ≡⟨ cong (_/ 2) (cong (_+ n * (n + 1)) (*-comm (suc n) 2)) 
    (2 * suc n + n * (n + 1)) / 2
  ≡⟨ cong (_/ 2) (cong (2 * suc n +_) (cong (n *_) (+-comm n 1))) 
    (2 * suc n + n * suc n) / 2
  ≡⟨ cong (_/ 2) (sym (*-distribʳ-+ (suc n) 2 n)) 
    (1 + suc n) * suc n / 2
  ≡⟨ cong (_/ 2) (cong (_* suc n) (+-comm 1 (suc n))) 
    (suc n + 1) * suc n / 2
  ≡⟨ cong (_/ 2) (*-comm (suc n + 1) (suc n)) 
    (suc n) * (suc n + 1) / 2
  

So now that we have both the base and inductive cases, let's combine it using this:

-- Given any natural number property (p :  → Set), if...
any-nat-prop : (p :   Set)
  -- ...it's true for the base case and...
   p 0
  -- ...it's true for the inductive case...
   ( {n : } (a : p n)  p (suc n))
  -- ...then the property is true for all naturals.
   ( (n : )  p n)
any-nat-prop p base _ zero = base
any-nat-prop p base ind (suc n) = ind (any-nat-prop p base ind n)

Then:

our-prop = any-nat-prop
  (λ n → sum-to-n n ≡ n * (n + 1) / 2)
  base-case inductive-case

Using Agda, we can see that this type-checks correctly.

TODO: Ensure totality

Higher inductive types

Moving on, we want to know what higher inductive types brings to the table. To illustrate its effect, let's consider the following scenario: suppose you have

References