How To Program Equivalence
Higher Inductive Type Approach
2022 James B. Wilson
Colorado State University
Problem: To a computer(...to you...), what is \[\mathbb{R}^4/\mathsf{Null}\left(\begin{bmatrix} 1 & 0 & 3 & 4 \\ 0 & 1 & 9 & -1\\0 & 0 & 0 & 0 \end{bmatrix}\right)?\]
- An equivalence relation \[u\equiv v\Leftrightarrow \begin{bmatrix}1& 0 & 3 & 4\\ 0 & 1 & 9 & -1\\ 0 & 0 & 0 & 0\end{bmatrix}(u-v)=0?\]
- The partition\[\left\{\left\{\begin{bmatrix}s\\ t\\ 0 \\ 0 \end{bmatrix} +\begin{bmatrix} -3 & -4\\ -9 & 1 \\ 1 & 0\\0 & 1\end{bmatrix}\begin{bmatrix}x\\y\end{bmatrix}\middle|x,y\in \mathbb{R}\right\}\middle|s,t\in\mathbb{R}\right\}?\]
- A function \(\pi:\mathbb{R}^4\to \mathbb{R}^2\) where \[\pi(v)=\begin{bmatrix}1 & 0 & 3 & 4 \\ 0 & 1 & 9 & -1\end{bmatrix}v?\]
Problem: To a computer(...to you...), what is \[\mathbb{Q}[x]/(2+x^3)?\]
- The partition\[\left\{a(x)+(2+x^3)\mid a(x)\in \mathbb{Q}[x]\right\}?\] Well computers can't really handle infinite sets.
- An equivalence relation \[a(x)\equiv b(x)\Leftrightarrow (2+x^3)\mid (a(x)-b(x))?\] Trouble is, the computer still sees \(9+x+x^{3}\) and \(11+x\) as different...and so do most programs that use that data.
- A function \(\pi:\mathbb{R}[x]\to \mathbb{M}_2(\mathbb{R})\) where \[\pi\left(\sum_{i} a_i x^9\right)=\sum_{i} a_i \begin{bmatrix} 0 & 0 & -2\\ 1 & 0 & 0\\ 0 & 1 & 0 \end{bmatrix}^i?\] Good enough, if you can guess such a function and don't mind the wasted memory and slower arithmetic.
Recall:
An Inductive Type Is "Polynomials"
(Under suitable conditions)
Polynomial=Induction
\((x+3)^2\)
\(x:=9\)
\((x+3)^2|_{x:=9} \rhd (9+3)^2\)
--- Borrow from context
import K:Comm, I:Type from Gamma
--- Make a new type
data Vec K I where ...
Free Module : FORMATION
\[\frac{K:Comm\qquad I:Set}{K^I:Type}\quad(F_{vec})\]
// Borrow from context
import K:Comm, I:Type from Gamma
// Make a new type
class Vec[K,I] { ... }
Procedural
Functional
\(K^I\)
\(I\)
\(\Gamma\vdash K,I\)
import K:Comm, I:Type from Gamma
data Vec K I where
Unit : {K:Comm} -> {I:Type} -> (i:I) -> (Unit K I)
> Unit Float (Fin 3) 2
Unit 2 : Unit Float (Fin 3)
Free Module : INTRODUCTION
\[\frac{i:I}{e_i:K^I}\quad(I_{e-vec})\]
import K:Comm, I:Type from Gamma
class Vec[K,I]
case class Unit[K,I](i:I) extends Vec[K,I]
// e_2 in R^3
> e2 = new Unit[Float,range(3)](2)
Procedural
Functional
\(K^I\)
\(I\)
\(e\)
\(\Gamma\vdash K,I\)
import K:Comm, I:Type from Gamma
data Vec K I where
Unit : {K:Comm} -> {I:Type} -> (i:I) -> (Vec K I)
Sum : (u:Vec K I) -> (v:Vec K I) -> (Vec K I)
Scl : (a:K) -> (u:Vec K I) -> (Vec K I)
Free Module : IMPLICIT INTRODUCTIONS
\[\frac{a:K\qquad u:K^I}{a*u:K^I}\quad(I_{*-vec})\]
import K:Comm, I:Type from Gamma
class Vec[K,I]
case class Unit[K,I](i:I) extends Vec[K,I]
case class Sum[K,I](u:Vec[K,I],v:Vec[K,I]) extends Vec[K,I]
case class Scl[K,I](a:K, u:Vec[K,I]) extends Vec[K,I]
Procedural
Functional
\[\frac{u,v:K^I}{u+v:K^I}\quad(I_{+-vec})\]
Multiple intros called "Inductive Type"
import K:Comm, I:Type from Gamma
data Vec K I where
Unit : (i:I) -> (Vec K I)
Sum : (u:Vec K I) -> (v:Vec K I) -> (Vec K I)
Scl : (a:K) -> (u:Vec K I) -> (Vec K I)
--- The Universal Mapping Property
umap:{U:Mod}->(Vec K I)->(f:I->U)->U
Free Module : ELIMINATION
import K:Comm, I:Type from Gamma
class Vec[K,I] {
// Universal Mapping Property
abstract def umap[U<:Mod[K]](f:I->U):U
}
case class Unit(i:I) extends Vec[K,I]
case class Sum(u:Vec[K,I],v:Vec[K,I]) extends Vec[K,I]
case class Scl(a:K, u:Vec[K,I]) extends Vec[K,I]
Procedural
Functional
\[\frac{v:K^I\quad U:{_K Mod}\quad f:I\to U}{\hat{f}(v):U}\quad(E_{vec})\]
\(K^I\)
\(I\)
\(e\)
\(U\)
\(\exists!\hat{f}\)
\(f\)
\(\Gamma\vdash K,I\)
import K:Comm, I:Type from Gamma
data Vec K I where
Unit : (i:I) -> (Vec K I)
Sum : (u:Vec K I) -> (v:Vec K I) -> (Vec K I)
Scl : (a:K) -> (u:Vec K I) -> (Vec K I)
umap:(U:Mod)->(Vec K I)->(f:I->U)->U
---Implement the computation
umap U (Unit i) f = f i
Free Module : COMPUTATION
import K:Comm, I:Type from Gamma
class Vec[K,I] {
abstract def umap[U<:Mod[K]](f:I->U):U
}
case class Unit(i:I) extends Vec[K,I] {
// Implement the computation
override def umap(f:I->U):U = f(i)
}
case class Sum(u:Vec[K,I],v:Vec[K,I]) extends Vec[K,I]
case class Scl(a:K, u:Vec[K,I]) extends Vec[K,I]
Procedural
Functional
\[\frac{i:I\quad U:{_K Mod}\quad f:I\to U}{\hat{f}(e_i)=f(i)}\quad(C_{e-vec})\]
\(K^I\)
\(I\)
\(e\)
\(U\)
\(\exists!\hat{f}\)
\(f\)
\(\Gamma\vdash K,I\)
import K:Comm, I:Type from Gamma
data Vec K I where
Unit : (i:I) -> (Vec K I)
Sum : (u:Vec K I) -> (v:Vec K I) -> (Vec K I)
Scl : (a:K) -> (u:Vec K I) -> (Vec K I)
--- The Universal Mapping Property
umap:(U:Mod)->(Vec K I)->(f:I->U)->U
umap U (Unit i) f = f i
umap U (Sum x y) f = (umap U x f)+(umap U y f)
umap U (Scl a x) f = a*(umap U x f)
Free Module : IMPLICIT COMPUTATION
import K:Comm, I:Type from Gamma
class Vec[K,I] {
abstract def umap[U<:Mod[K]](f:I->U):U
}
case class Unit(i:I) extends Vec[K,I] {
override def umap(f:I->U):U = f(i)
}
case class Sum(u:Vec[K,I],v:Vec[K,I]) extends Vec[K,I] {
override def umap(f:I->U):U = u.umap(f)+v.umap(f)
}
case class Scl(a:K, u:Vec[K,I]) extends Vec[K,I]{
override def umap(f:I->U):U = a*u.umap(f)
}
Procedural
Functional
\[\frac{u,v:K^I\quad U:{_K Mod}\quad f:I\to U}{\hat{f}(u+v)=f(u)+f(v)}\quad(C_{+-vec})\]
\[\frac{a:K\quad v:K^I\quad U:{_K Mod}\quad f:I\to U}{\hat{f}(a*v)=a*f(v)}\quad(C_{*-vec})\]
Summary
- Functions defined from recursion are "unique" only if we compare function input-to-ouput (extensional) instead of step-by-step (intentional)
- Extensional Type Theory undecidable.
- Intentional Type Theory decidable but recursion has room for programs to tinker (Loops, folds, zips, trampolines...)
Technicalities
- UMPs give types
- Free UMP gives inductive type
Idea:
A Quotient Type Is "Polynomials with rewriting rules"
That's a lousy solution
Under the hood
// [ 3.14159, 2.71828]
v = Sum( Scl(3.14159, Unit(1)), Scl(2.71828, Unit(2))
Bulky syntax, but that can be fixed with a function; let that go.
HEAP
type:"Scl099F" a0001: 0000 0003, 0000 374F v0001: 7F66 A008
type:"Tree07A2"
l0001: 7F66 9800
r0001: 7F66 A000
type:"class" name:"Vec001B" K2B01:"Float64" I0458:"FinAA03" meth1:"eval" +p1:"func"<X,Y> +p2:K2B01 -r:<Y>
type:"Vec001B"
K2B01:"Float64"
I0458:"FinAA03"
type:"class"
name:"Unit70E0"
sup:"Vect001B"
type:"Vec001B"
K2B01:"Float64"
I0458:"FinAA03"
type:"class"
name:"FinAA03"
I0001:0000 0003
....
type:"Unit07A2"
i0001: 0000 0001
type:"Unit07A2"
i0001: 0000 0002
type:"Scl099F" a0001: 0000 0002, 0001 1894 v0001: 7F66 A001
type:"foo9056"
...
type:"foo9056"
...
type:"foo9056"
...
type:"bazEE01"
...
type:"Tree07A2"
l0001: 7F66 9805
r0001: 7F66 A001
type:"Tree07A2"
l0001: 7F66 9801
r0001: 7F66 98A2
....
What you intend
// [ 3.14159, 2.71828]
v = Sum( Scl(3.14159, Unit(1)), Scl(2.71828, Unit(2))
HEAP
type:"Tree07A2"
l0001: 7F66 9800
r0001: 7F66 A000
type:"class" name:"Vec001B" K2B01:"Float64" I0458:"FinAA03" meth1:"eval" +p1:"func"<X,Y> +p2:K2B01 -r:<Y>
type:"Vec001B"
K2B01:"Float64"
I0458:"FinAA03"
type:"class"
name:"Unit70E0"
sup:"Vect001B"
type:"Vec001B"
K2B01:"Float64"
I0458:"FinAA03"
type:"class"
name:"FinAA03"
I0001:0000 0003
....
type:"vev009E"
a0001: 0000 0003, 0000 374F a0001: 0000 0002, 0001 1894
type:"foo9056"
...
type:"foo9056"
...
type:"foo9056"
...
type:"bazEE01"
...
type:"Tree07A2"
l0001: 7F66 9805
r0001: 7F66 A001
....
Many designs
- Blocks: keeping data together that is used together
- Packing: fill in the 0000
- Packeting: Size data to move through machine fast.
- Flyweights: scrapping headers
- ...branch predictions, precomputing, reusing ...
Our design is terrible
So why are we so smug?
Scenario
- We made a free-module type: MyVec
- Engineers will make MUCH better ones: FactVec
- PROBLEM: convert between them.
Mathematical detour
Prop. Any two free modules on \(I\) are naturally isomorphic.
Proof. Let \(\langle F_k, e^{(k)}:I\to F_k\rangle\) be free.
Then \(e^{(2)}:I\to F_2\) induces a linear map \(\hat{e}_2:F_1\to F_2\) and vice-versa \(\hat{e}^{(1)}:F_2\to F_1\). Furthermore \(\hat{e}_1\circ \hat{e}_2\circ e^{(1)}=e^{(1)}\). But so does the identity, so by uniqueness of UMP, \[\hat{e}_1\circ \hat{e}_2=id_{F_1}.\] Likewise if we reverse the composition. So these functions are isomorphisms.
Return to Code
Prop. Any two free modules on \(I\) are naturally isomorphic.
Proof. Let \(\langle F_k, e^{(k)}:I\to F_k\rangle\) be free.
Then \(e^{(2)}:I\to F_2\) induces a linear map \(\hat{e}_2:F_1\to F_2\) and vice-versa \(\hat{e}^{(1)}:F_2\to F_1\). Furthermore \(\hat{e}_1\circ \hat{e}_2\circ e^{(1)}=e^{(1)}\). But so does the identity, so by uniqueness of UMP, \[\hat{e}_1\circ \hat{e}_2=id_{F_1}.\] Likewise if we reverse the composition. So these functions are isomorphisms.
PROOFS CARRY ALGORITHMIC CONTENT
(Curry-Howard Isomorphism Theorem)
Solution (from proof)
> my_u:MyVec[K,I] = ...
> f:I->YourVec[K,I] = i -> YourUnit[K,I](i)
> your_u = my_u.ump(f)
Prop. Any two free modules on \(I\) are naturally isomorphic.
Proof. Let \(\langle F_k, e^{(k)}:I\to F_k\rangle\) be free.
Then \(e^{(2)}:I\to F_2\) induces a linear map \(\hat{e}_2:F_1\to F_2\) and vice-versa \(\hat{e}^{(1)}:F_2\to F_1\). Furthermore \(\hat{e}_1\circ \hat{e}_2\circ e^{(1)}=e^{(1)}\). But so does the identity, so by uniqueness of UMP, \[\hat{e}_1\circ \hat{e}_2=id_{F_1}.\] Likewise if we reverse the composition. So these functions are isomorphisms.
Summary
- Start with UMP, even a basic implementation
- Later upgrades achieved automatically by UMP theory of essential uniqueness
- Other UMP properties:
- Preserved under functors \(\Rightarrow\) Less re-programming.
- Unified naming/conventions \(\Rightarrow\) Less fatal style wars; easier to read other's work, easy to pass on.
Higher Inductive Types
By James Wilson
Higher Inductive Types
Using universal mapping properties to prescribe data types and using resulting theory to build useful algorithms.
- 430