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.