Programming with Universal Mapping Properties
2021 James B. Wilson
Colorado State University
Academic Programmer's Quandry
Where to start?
What to make first?
Any "real-world" problem?
How to reuse what exists?
Can I publish a paper?
Is it "real-world" experience?
Is it math?
Can I teach it to a student?
Will my advisor understand?
Where's a proof?
How to finish it?
Will a math system take it?
FACT (Grace Hopper).
Programming is Math.
Programming Idioms: logic written in simulate human language
\[\forall x.(1\leq x\leq 5\Rightarrow ...)\]
Public Domain, original copyright (c) James S. Davis
for x in [1..5] ...
Programming "Idioma" Language
Collection of idioms that can
model a symbolic logic.
Theorem (Curry-Howard).
Propositions=Data Types
Proofs=Algorithms*
Prop. (Division Algorithm).
For every \(n\in \mathbb{N}, m\in \mathbb{N}^+\) there exists \(q\in \mathbb{N},r\in \mathbb{N}_{<m}\) where \[n=qm+r.\]
Proof.
def div(n:Nat,m:PosNat): (Nat,Fin m) =
if n < m then
(0,n)
else
(1,0) + div(n-m,m)
By Gleb.svechnikov - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=58344593
William Howard
(no photo)
Haskell Curry
*So long as you phrase contradictions in the negative.
\(div:\mathbb{N}\times\mathbb{N}_{>0}\to \mathbb{N}\times \mathbb{N}_{<m}\)
Can be more precise:
\[\begin{aligned} div&:\mathbb{N}\times\mathbb{N}_{>0}\to \\ & \bigcup_{q\in \mathbb{N}}\bigcup_{r\in \mathbb{N}_{<m}} n=qm+r\end{aligned}\]
where \(n=qm+r\) is the type of data that proves equality (e.g. same place in memory; equal arithmetic circuits, etc.).
Types 101
Types are annotations to data that imply the data be used strictly by fixed rules.
E.g. Annotations
\[5\in \mathbb{Z}\qquad 5:\mathbb{Z}\qquad 5^{\mathbb{N}}\]
int x
x:Int
x // x is an integer.
Types are just documentation!
Some Prog. Lang. read these docs to double-check "type-check"; others assume the best and wait for a problem.
'5':Char, 2:Int means '5'+2 is an error;
5:Int, 2:Int allows 5+2.
Universal Mapping Properties (UMP) Yield Data Types
--- 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
- Each UMP natural writes its own program.
- Even polar opposite Programming Conventions look the same when implementing UMPs
- The code is quite skinny.
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: Vec
- Engineers will make MUCH better ones: FastVec
- 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.
Programming with Universal Mapping Properties
By James Wilson
Programming with Universal Mapping Properties
Using universal mapping properties to prescribe data types and using resulting theory to build useful algorithms.
- 393