blog/content/posts/2022-09-20-higher-inductive-types.lagda.md

190 lines
5.6 KiB
Markdown
Raw Normal View History

2022-09-26 17:58:51 +00:00
+++
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.
<!--more-->
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`):
```agda
data Nat : Set where
zero : Nat
suc : Nat → Nat
```
This defines all `Nat`s 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:
```txt
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:
```agda
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:
<details>
<summary>(click here for boring requisites)</summary>
```agda
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
```
</details>
```agda
-- 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
```
<details>
<summary>Inductive case proof, expand if you're interested</summary>
```agda
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
```
</details>
So now that we have both the base and inductive cases, let's combine it using
this:
```agda
-- 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
- [nLab](https://ncatlab.org/nlab/show/higher+inductive+type)
- Section 2.5 of [Favonia's thesis](https://favonia.org/files/thesis.pdf)
- [Quotient Types for Programmers](https://www.hedonisticlearning.com/posts/quotient-types-for-programmers.html)