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.

  • 314