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