oplss2024/pfenning/notes.typ

377 lines
11 KiB
Text
Raw Normal View History

2024-06-03 19:00:06 +00:00
#import "../common.typ": *
2024-06-04 14:31:23 +00:00
#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(\&)$
2024-06-03 19:00:06 +00:00
= 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(())$])
- +
2024-06-04 14:31:23 +00:00
- #tree(axi[$e : A$], uni[$"inl" e : A + B$])
- #tree(axi[$e : B$], uni[$"inr" e : A + B$])
2024-06-03 19:00:06 +00:00
- #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)$],
)
2024-06-04 14:31:23 +00:00
2024-06-04 17:52:15 +00:00
#pagebreak()
2024-06-04 14:31:23 +00:00
== 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)$]
)