5.6 KiB
+++ 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 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:
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
- nLab
- Section 2.5 of Favonia's thesis
- Quotient Types for Programmers