375 lines
No EOL
11 KiB
Text
375 lines
No EOL
11 KiB
Text
#import "../common.typ": *
|
|
#import "@preview/prooftrees:0.1.0": *
|
|
#import "@preview/algo:0.3.3": algo, i, d, comment, code
|
|
#show: doc => conf(doc)
|
|
|
|
#let n(t) = text(fill: rgb("#aa3333"))[#t]
|
|
#let evalto = $arrow.r.hook$
|
|
#let with = $op(\&)$
|
|
|
|
= Adjoint Functional Programming
|
|
|
|
== Lecture 1. Linear functional programming
|
|
|
|
- Origins of linearity is from linear logic
|
|
- from 1987 or before
|
|
|
|
#let isValue(x) = $#x "value"$
|
|
|
|
Language to be studied is called "snax"
|
|
- Features
|
|
- Substructural programming
|
|
- Inference
|
|
- Overloading? writing the same function that works both linearly and non-linearly
|
|
- "Proof-theoretic compiler"
|
|
- Everything up til the target C is either natural deduction, sequent calculus, etc.
|
|
- Types
|
|
- "if there's something you don't need in the language, don't put it in the language"
|
|
- no empty type
|
|
- unit type
|
|
- 1
|
|
- #tree(axi[], uni[$() : 1$])
|
|
- #tree(axi[], uni[$isValue(())$])
|
|
- +
|
|
- #tree(axi[$e : A$], uni[$"inl" e : A + B$])
|
|
- #tree(axi[$e : B$], uni[$"inr" e : A + B$])
|
|
- #tree(axi[$isValue("v")$], uni[$isValue("inl" v)$])
|
|
- #tree(axi[$isValue("v")$], uni[$isValue("inr" v)$])
|
|
- $A + B :equiv +\{"inl":A, "inr": B\}$
|
|
- label set $L eq.not emptyset$ , finite
|
|
- finiteness is important
|
|
- nat = $+\{ "zero" : 1 , "succ" : "nat" \}$
|
|
- recursion is required to define an infinite amount of things
|
|
- "equirecursive" types
|
|
- nat is "equal" to the recursive definition in some sense
|
|
- k
|
|
- $\{(k \in l) e : A_e}{k(e) : +\{l : A_l\}_{l : L}}$
|
|
- $ceil(0) = "zero" ()$
|
|
|
|
- Computational rules
|
|
|
|
Define negation of booleans
|
|
|
|
$"not" (x : "bool") : "bool" \
|
|
"not" x = "match" x "with" \
|
|
| "true" u arrow.r "false" u\
|
|
| "false" u arrow.r "true" u \
|
|
$
|
|
|
|
Function definitiosn like these operate at the meta level.
|
|
Avoid introducing function types today.
|
|
|
|
Linear functional programming means *every variable must be used exactly once.*
|
|
|
|
*NOTE:* Above cannot be defined $"true" u arrow.r "false" ()$, since $u$ is not used.
|
|
|
|
DOn't need a garbage collector for linear functional programming language
|
|
|
|
#tree(
|
|
axi[$e : +{l : A_l}_(l in L)$],
|
|
axi[$x : A_l$],
|
|
axi[$tack.r e_l : C(l in L)$],
|
|
nary(3)[$"match" e "with" (l(x) arrow.r.double e_l)_(l in L) : C$],
|
|
)
|
|
|
|
Cannot write a function $"duplicate"(x) = (x , x)$ because of linear programming
|
|
|
|
#tree(
|
|
axi[$Delta tack.r e_1: A$],
|
|
axi[$Gamma tack.r e_2 : B$],
|
|
bin[$Delta,Gamma tack.r (e_1, e_2) : A times B$],
|
|
)
|
|
|
|
so introducing contexts
|
|
|
|
$Gamma :equiv (dot) | Gamma , x : A$
|
|
|
|
where the variable $x$ must be unique.
|
|
|
|
#tree(axi[], uni[$x : A tack.r x : A$])
|
|
|
|
*NOTE:* Left side is $x : A$, not $Gamma , x : A$ because those others would be unused, violating the single use rule.
|
|
|
|
#tree(
|
|
axi[$Delta , e : A times B$],
|
|
axi[$Gamma , x : A, y : B tack.r e' : C$],
|
|
nary(2)[$Delta , Gamma tack.r "match" e "with" (x , y) => e' : C$],
|
|
)
|
|
|
|
Have to split your variables to check $e$ and $e'$. This is not the way to implement it.
|
|
|
|
To implement it, check $"FV"(e)$ and $"FV"(e')$. These correspond to $Delta$ and $Gamma$ respectively. This is expensive.
|
|
|
|
- An easier way is to have each judgement give back another context containing the variables not use. Called a "subtractive" approach.
|
|
|
|
- Additive approach returns the variables used, and then check to make sure variables don't exist on the output.
|
|
|
|
GOing backk
|
|
|
|
#tree(
|
|
axi[],
|
|
uni[$dot tack.r () : 1$],
|
|
)
|
|
|
|
// #tree(
|
|
// axi[$Delta tack.r e : +{l : A_l}_(l in L)$],
|
|
// axi[$Gamma $]
|
|
// )
|
|
|
|
Defining plus:
|
|
|
|
-
|
|
$"plus" (x : "nat") (y : "nat") : "nat" \
|
|
"plus" x " " y = "match" x "with" \
|
|
| "zero" () arrow.r.double #text(fill: red)[$"zero" ()$] \
|
|
| "succ" x' arrow.r.double "succ" ("plus" x' y)
|
|
$
|
|
|
|
This is problematic because it doesn't use up $y$ in the first branch.
|
|
|
|
-
|
|
$"plus" (x : "nat") (y : "nat") : "nat" \
|
|
"plus" x " " y = "match" x "with" \
|
|
| "zero" () arrow.r.double y \
|
|
| "succ" x' arrow.r.double "succ" ("plus" x' y)
|
|
$
|
|
|
|
This uses up all inputs exactly.
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e : A_k (k in L)$],
|
|
uni[$Gamma tack.r k(e) : +{l : A_l}_(l in L)$],
|
|
)
|
|
|
|
== Lecture 2
|
|
|
|
Type system
|
|
|
|
- Proving something different than type soundness; we want to demonstrate that no garbage is created.
|
|
- Set up a close correspondence between the static rules for inference and dynamic rules for evaluation.
|
|
- This should be _very_ close ideally.
|
|
|
|
At runtime, if you have $Gamma tack.r e : A$
|
|
- $e$ is what you're executing
|
|
- you don't want to carry $A$ around during runtime
|
|
- $Gamma$ corresponds to a set of variables (you need this) $eta : Gamma$
|
|
|
|
#rect[$eta : Gamma$]
|
|
|
|
#tree(axi[], uni[$(dot) : (dot)$])
|
|
#tree(axi[$eta : Gamma$], axi[$dot tack.r v : A$], bin[$eta , x mapsto v : Gamma , x : A$])
|
|
|
|
("environment" refers to $eta$, while context refers to $Gamma$)
|
|
|
|
Runtime values are _always_ closed.
|
|
|
|
*Theorem.*
|
|
Under the conditions $Gamma tack.r e : A$ and $eta : Gamma$, then $eta tack.r e arrow.r.hook v$ and $dot tack.r v : A$.
|
|
|
|
However, this rule is problematic:
|
|
|
|
#tree(
|
|
axi[$Delta tack.r e_1 : A$],
|
|
axi[$Gamma tack.r e_2 : B$],
|
|
bin[$Delta,Gamma tack.r (e_1, e_2) : A times B$]
|
|
)
|
|
|
|
This evaluates to:
|
|
|
|
#tree(
|
|
axi[$e_1 arrow.r.hook v_1$],
|
|
axi[$e_2 arrow.r.hook v_2$],
|
|
bin[$eta tack.r (e_1, e_2) arrow.r.hook (v_1, v_2)$],
|
|
)
|
|
|
|
How to split the environment in this case?
|
|
It's not feasible to split the free variables.
|
|
|
|
One idea is we can just pass the whole environment into both sides:
|
|
|
|
#tree(
|
|
axi[$eta tack.r e_1 arrow.r.hook v_1$],
|
|
axi[$eta tack.r e_2 arrow.r.hook v_2$],
|
|
bin[$eta tack.r (e_1, e_2) arrow.r.hook (v_1, v_2)$],
|
|
)
|
|
|
|
The typechecker tells us that everything is used up correctly, so at runtime we can use this assumption.
|
|
Using subtractive approach, this looks like:
|
|
|
|
#tree(
|
|
axi[$eta tack.r e_1 arrow.r.hook v_1 #n[$tack.l eta_1$]$],
|
|
axi[$#n[$eta_1$] tack.r e_2 arrow.r.hook v_2 #n[$tack.l eta_2$]$],
|
|
bin[$#n[$eta_2$] tack.r (e_1, e_2) arrow.r.hook (v_1, v_2)$],
|
|
)
|
|
|
|
The subtractive approach has problems:
|
|
|
|
- When you execute your program, you are forced to go left-to-right. You can't run in parallel.
|
|
|
|
Additive approach:
|
|
|
|
#rect[$Gamma tack.r e: A tack.l Omega$]
|
|
|
|
where $Omega$ are the variables that are _actually_ used
|
|
|
|
For example, rules for pairs:
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e_1 : A tack.l Omega_1$],
|
|
axi[$Gamma tack.r e_2 : B tack.l Omega_2$],
|
|
bin[$Gamma tack.r (e_1,e_2) : A times B tack.l Omega_1, Omega_2$],
|
|
)
|
|
|
|
The $Omega_1 , Omega_2$ disjoint union, and is undefined if they share a variable.
|
|
|
|
The runtime rule corresponds to:
|
|
|
|
#tree(
|
|
axi[$eta tack.r e_1 arrow.r.hook v_1 tack.l omega_1$],
|
|
axi[$eta tack.r e_2 arrow.r.hook v_2 tack.l omega_2$],
|
|
bin[$eta tack.r (e_1, e_2) arrow.r.hook (v_1, v_2) tack.l omega_1, omega_2$]
|
|
)
|
|
|
|
If we typechecked correctly, $omega_1$ and $omega_2$ would be disjoint as well.
|
|
|
|
*Soundness.* If $Gamma tack.r e : A tack.l Omega$ then $Omega tack.r e : A$ ($Omega subset Gamma$)
|
|
|
|
*Completeness.* If $Omega tack.r e : A$ and $Gamma supset Omega$ then $Gamma tack.r e : A tack.l Omega$
|
|
|
|
The $Omega tack.r e : A$ is a _precise_ judgement: $Omega$ _only_ contains the variables used in $e$.
|
|
|
|
You would need two different sets of typing rules, one that has the $Omega$ rules and one that doesn't.
|
|
Prove this by rule induction.
|
|
|
|
Change the original theorem:
|
|
|
|
*New Theorem.*
|
|
Under the conditions $Gamma tack.r e : A tack.l Omega$ and $eta : Gamma$ and $omega : Omega$,
|
|
then $eta tack.r e arrow.r.hook v tack.l omega$ and $dot tack.r v : A$.
|
|
|
|
This can now be proven with the new dynamics.
|
|
|
|
How to prove that there's no garbage?
|
|
For all top level computations, $eta tack.r e arrow.r.hook v tack.l eta$.
|
|
This means that everything is used.
|
|
This can be proven using rule induction.
|
|
|
|
Updated theorem, to point out that $e arrow.r.hook v$ may not necessarily be true because termination is not proven by rule induction.
|
|
|
|
*New Theorem.*
|
|
Under the conditions $Gamma tack.r e : A tack.l Omega$ and $eta : Gamma$ and $omega : Omega$,
|
|
#n[and $eta tack.r e arrow.r.hook v tack.l omega$] then $dot tack.r v : A$.
|
|
|
|
(under affine logic, the top-level judgment $Gamma tack.r e : A tack.l Omega$ requires only that $Omega subset Gamma$, not that $Omega = Gamma$)
|
|
|
|
Example of evaluation rule that matches the typing rule:
|
|
|
|
#tree(
|
|
axi[$eta tack.r e arrow.r.hook (v_1, v_2) tack.l omega$],
|
|
axi[$eta, x mapsto v_1 , y mapsto v_2 tack.r e' arrow.r.hook v' tack.l (omega', x mapsto v_1, y mapsto v_2)$],
|
|
bin[$eta tack.r "match" e "with" (x, y) arrow.r.double e' arrow.r.hook v' tack.l (omega, omega')$]
|
|
)
|
|
|
|
|
|
=== Looking at typing rules as logic
|
|
|
|
#rect[Natural Deduction]
|
|
|
|
$Gamma ::= (dot) | Gamma , A$
|
|
|
|
#tree(
|
|
axi[],
|
|
uni[$A tack.r A$]
|
|
)
|
|
|
|
#tree(
|
|
axi[$Delta tack.r A$],
|
|
axi[$Gamma tack.r B$],
|
|
bin[$Delta, Gamma, tack.r A times B$]
|
|
)
|
|
|
|
#tree(
|
|
axi[$Delta tack.r A times B$],
|
|
axi[$Gamma, A, B tack.r C$],
|
|
bin[$Delta, Gamma tack.r C$]
|
|
)
|
|
|
|
These are like a rule of logic. For plus:
|
|
|
|
#tree(
|
|
axi[$Delta tack.r A$],
|
|
uni[$Delta tack.r A + B$],
|
|
)
|
|
|
|
#tree(
|
|
axi[$Delta tack.r B$],
|
|
uni[$Delta tack.r A + B$],
|
|
)
|
|
|
|
Proof by cases:
|
|
|
|
#tree(
|
|
axi[$Delta tack.r A + B$],
|
|
axi[$Gamma , A tack.r C$],
|
|
axi[$Gamma , B tack.r C$],
|
|
tri[$Delta , Gamma tack.r C$],
|
|
)
|
|
|
|
Every assumption has to be used _exactly_ once. This is called *linear logic*.
|
|
|
|
(notation uses $times.circle$ and $plus.circle$ instead of $times$ and $plus$)
|
|
|
|
Linear logic is weak by itself, just as linear type system is weak without global definitions.
|
|
|
|
Summary of operators:
|
|
|
|
#image("lec1.jpg")
|
|
|
|
$!A$ is read "of course A", lets you re-use assumptions. We don't have this except by top-level definitions.
|
|
|
|
In the judgement $Sigma ; Gamma tack.r e : A$, $Sigma$ contains definitions that you can use however many times you want, and $Gamma$ contains the definitions that are created linearly.
|
|
|
|
Distinction between positive and negative types:
|
|
|
|
- Lambdas cannot be pattern-matched against, you have to apply it.
|
|
- However, for $times.circle$ and $plus.circle$ you can directly observe their structure.
|
|
|
|
In this case, $A with B$, read "A with B":
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r A$],
|
|
axi[$Gamma tack.r B$],
|
|
bin[$Gamma tack.r A with B$],
|
|
)
|
|
|
|
This is sound because only one of them can be extracted:
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e_1:A$],
|
|
axi[$Gamma tack.r e_2:B$],
|
|
bin[$Gamma tack.r angle.l e_1,e_2 angle.r : A with B$],
|
|
)
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e : A with B$],
|
|
uni[$Gamma tack.r e.pi_1 : A$]
|
|
)
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e : A with B$],
|
|
uni[$Gamma tack.r e.pi_2 : B$]
|
|
)
|
|
|
|
"Lazy pair" you can only extract one side at a time. There are also "lazy records":
|
|
|
|
$with { l : A_l}_(l in L)$
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e_l : A_l (forall l in L)$],
|
|
uni[$Gamma tack.r {l = e_l}_(l in L) : with {l : A_l}_(l in L)$]
|
|
)
|
|
|
|
#tree(
|
|
axi[$Gamma tack.r e : with { l : A_l}_(l in L) (forall l in L)$],
|
|
uni[$Gamma tack.r e.k : A_k (k in L)$]
|
|
) |