<style> .reveal { font-size: 32px; } </style> # Why Type Systems are Great ### <small>(when combined with a sufficiently powerful typechecker)</small> <!-- Put the link to this slide here so people can follow --> slides: https://tinyurl.com/types-are-awesome code: https://github.com/sagnibak/types-are-awesome-demo --- In order to make the most of this talk, you must be able to appreciate formal mathematical proofs. <span>That is not a problem since you've all taken CS 70.<!-- .element: class="fragment" data-fragment-index="1" --></span> ---- ## Proof <span>In order to become a UPE member, you must be a CS major. <!-- .element: class="fragment" data-fragment-index="1" --></span><span>In order to be a CS major, you must have taken CS 70. <!-- .element: class="fragment" data-fragment-index="2" --></span><span>Therefore every UPE member must have taken CS 70.<!-- .element: class="fragment" data-fragment-index="3" --></span> ---- ## A Program ```python class UPEMember: csMajor: CSMajor class CSMajor: takenCS70: HasTakenCS70 class HasTakenCS70: sid: int def upeToCS70(upeMember: UPEMember) -> HasTakenCS70: return csMajorToCS70(upeMember.csMajor) def csMajorToCS70(csMajor: CSMajor) -> HasTakenCS70: return csMajor.takenCS70 ``` ---- <!-- $$ \text{Jay is a UPE Member} \implies \text{Jay has taken CS 70} $$ --> Jay is a UPE member $\implies$ Jay has taken CS 70. ```python jay_monga = UPEMember(CSMajor(HasTakenCS70(3032323332))) also_jay: HasTakenCS70 = upeToCS70(jay_monga) ``` <span><tr> - All the code type-checks!<!-- .element: class="fragment" data-fragment-index="1" --></tr></span> <span><tr> - Proves that the set of UPE members is nonempty $\implies$ the set of students who have taken CS 70 is nonempty.<!-- .element: class="fragment" data-fragment-index="2" --></tr></span> --- ## The Curry-Howard Isomorphism Every proposition is isomorphic to a type. Every proof is isomorphic to a program. ---- | Logic | Python | C++ | Rust | Scala | Haskell | |-------|--------|------|------------|--------|-----------| | True | `None` | `void` | `()` | `Unit` | `()` | | False | `NoReturn` | `void` | `!`/`never` | `Nothing` | `bottom` | | $\land$ | `(a, b)` | `pair` | `(a, b)` | `(a, b)` | `(a, b)` | | $\lor$ | `Union` | tagged union | `enum` | `Either` | `Either` | | $\implies$ | `def` | function | `fn` | `def` | function | | $\forall$ | `Generic` | `<...>` | `<...>` | `[...]` | generics | <!-- | Logic | Python | C++ | Rust | Scala | Haskell | |-------|--------|------|------------|--------|-----------| | True | `None` | `void` | `()` | `Unit` | `()` | | False | `NoReturn` | `void` | `!`/`never` | `Nothing` | `bottom` | | $\land$ | classes, tuples, structs | | $\lor$ | `Union` | tagged union | `enum` | `Either` | `Either` | | $\implies$ | `def` | function | `fn` | `def` | function | | $\forall$ | `Generic` | `<...>` | `<...>` | `[...]` | generics | --> ...and a LOT more! ---- ### Negation \begin{align*} \neg P &\equiv \neg P \lor \text{False} \\ &\equiv (P \implies \text{False}) \end{align*} We can use this tautology to define the negation of a type. Python: ```python notP = Callable[[P], NoReturn] # this is a type alias ``` Rust: ```rust type Not<P> = fn(P) -> !; // type alias ``` ---- ### Distributive Law $(P \lor Q) \land R \implies (P \land R) \lor (Q \land R)$ Python: ```python def distribute(p_or_q: Union[P, Q], r: R) -> Union[Tuple[P, R], Tuple[Q, R]]: if isinstance(p_or_q, P): p: P = p_or_q return (p, r) else: q: Q = p_or_q return (q, r) ``` Rust: ```rust fn distribute(p_or_q: Either<P, Q>, r: R) -> Either<(P, R), (Q, R)> { match p_or_q { Either::Left(p) => Either::Left((p, r)), Either::Right(q) => Either::Right((q, r)), } } // defining a sum type enum Either<P, Q> { Left(P), Right(Q), } ``` ---- ### Function Application, Modus Ponens $((P \implies Q) \land P) \implies Q$ Python: ```python def apply(p_to_q: Callable[[P], Q], p: P) -> Q: return p_to_q(p) ``` Rust: ```rust fn apply(p_to_q: fn(P) -> Q, p: P) -> Q { p_to_q(p) } ``` <span>If I have a way to convert evidence for $P$ into evidence for $Q$ and I have evidence for $P$, then I can produce evidence for $Q$.<!-- .element: class="fragment" data-fragment-index="1" --></span> ---- ### Currying $((P \land Q) \implies R) \implies (P \implies (Q \implies R))$ ```python def curry(fn: Callable[[P, Q], R]) -> Callable[[P], Callable[[Q], R]]: def curried(p: P) -> Callable[[Q], R]: def inner(q: Q) -> R: return fn(p, q) return inner return curried ``` ---- ### Universal Quantification, Generics $(P \land Q) \implies (Q \land P) \; \forall P, Q$ ```rust fn and_commutes<P, Q>(p_and_q: (P, Q)) -> (Q, P) { (p_and_q.1, p_and_q.0) } ``` <span> We can actually make all the functions above generic by adding `<...>`, just like we can make the propositions true $\forall \ldots$ <!-- .element: class="fragment" data-fragment-index="1" --></span> <span> ```rust fn notP<P>(p: P) -> ! { loop {} // infinite looping is also valid } ``` <!-- .element: class="fragment" data-fragment-index="2" --> ```rust fn distribute<P, Q, R>( p_or_q: Either<P, Q>, r: R ) -> Either<(P, R), (Q, R)> { match p_or_q { Either::Left(p) => Either::Left((p, r)), Either::Right(q) => Either::Right((q, r)), } } ``` <!-- .element: class="fragment" data-fragment-index="2" --> </span> ---- ### Contraposition $(P \implies Q) \implies (\neg Q \implies \neg P)$ ```python NotP = Callable[[P], NoReturn] NotQ = Callable[[Q], NoReturn] def contraposition(p_to_q: Callable[[P], Q]) -> Callable[[NotQ], NotP]: def contrapositive(not_q: NotQ) -> NotP: def inner(p: P) -> NoReturn: return not_q(p_to_q(p)) return inner return contrapositive ``` ---- ### Double Negation Introduction \begin{align*} P &\implies \neg \neg P \\ \equiv P &\implies (\neg P \implies \text{False}) \\ \equiv P &\implies ((P \implies \text{False}) \implies \text{False}) \end{align*} ```python NotP = Callable[[P], NoReturn] NotNotP = Callable[[NotP], NoReturn] def double_negation_intro(p: P) -> NotNotP: def double_negation(not_p: NotP) -> NoReturn: return not_p(p) return double_negation ``` ---- ### Double Negation Elimination $\neg \neg P \implies P$ Impossible to prove! <span> You only have functions that return instances of `NoReturn`. They don't give you any way to produce evidence for $P$.<!-- .element: class="fragment" data-fragment-index="1" --></span> <span>You typically need language extensions (e.g., in Haskell) to add this as an axiom.<!-- .element: class="fragment" data-fragment-index="2" --></span> ---- ### Proof by Contradiction $(\neg P \implies \text{False}) \implies P$ Impossible to prove, since it implies (and is implied by) double negation in this framework of logic. ---- ### No law of the excluded middle $$ P \lor \neg P \not\Rightarrow \text{True} $$ <span> Equivalent to double negation elimination! \begin{align*} P \lor \neg P &\equiv \neg P \lor P \\ &\equiv \neg \neg P \implies P \end{align*} <!-- .element: class="fragment" data-fragment-index="1" --></span> <span>Topology has scenarios where the law of the excluded middle does not hold. Scroll down with caution.<!-- .element: class="fragment" data-fragment-index="2" --></span> ---- <span> - Consider the Sierpiński topological space, defined by the set $X:=\{0,1\}$ with topology (open sets) $\mathcal{T} := \{\varnothing, \{1\}, \{0,1\}\}$. Then the closed sets are $\{\varnothing, \{0\}, \{0,1\}\}$. <!-- .element: class="fragment" data-fragment-index="1" --></span> <span> - If we let $X$ be true, $\varnothing$ be false, $\cup$ be or, and $\cap$ be and, we get a system that obeys the axioms of intuitionistic logic. Every proposition is a set, and the **interior** of its complement is its negation. <!-- .element: class="fragment" data-fragment-index="2" --></span> <span> - LEM is equivalent to $U \cup \neg U = X\;\forall U\subseteq X$, but this does not hold for $U := \{1\}$, since $\neg U = \text{interior}(\{1\}^c) = \varnothing$.<!-- .element: class="fragment" data-fragment-index="3" --> </span> ---- <span> - Consider $X = \mathbb{R}$ with the usual topology (open balls/open rectangles). <!-- .element: class="fragment" data-fragment-index="1" --></span> <span> - If we let $X$ be true, $\varnothing$ be false, $\cup$ be or, and $\cap$ be and, we get a system that obeys the axioms of intuitionistic logic. Every proposition is a set, and the **interior** of its complement is its negation. <!-- .element: class="fragment" data-fragment-index="2" --></span> <span> <span> - LEM is equivalent to $U \cup \neg U = X\;\forall U\subseteq X$, but this does not hold for $U := (p, q)$, since $\neg U = \text{interior}((-\infty, p] \cup [q, \infty)) = (-\infty, p) \cup (q, \infty)$. - $(p, q) \cup (-\infty, p) \cup (q, \infty) = \mathbb{R} \setminus \{p, q\} \ne \mathbb{R}$ <!-- .element: class="fragment" data-fragment-index="3" --> </span> --- ## But Why Does It Matter? - More work at compile time means less work at runtime - Code that compiles usually works --- ## Example: `isinstance` <span> Without types: <!-- .element: class="fragment" data-fragment-index="1" --> ```python def csMajorToCS70(csMajor): if isinstance(csMajor, CSMajor): return csMajor.takenCS70 else: raise TypeError("csMajor must be a CSMajor!") ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> With types: <!-- .element: class="fragment" data-fragment-index="2" --> ```python def csMajorToCS70(csMajor: CSMajor) -> HasTakenCS70: return csMajor.takenCS70 ``` <!-- .element: class="fragment" data-fragment-index="2" --> </span> ---- We need an `isinstance` check (runtime typechecking) when we don't have compile-time typechecking. This is bad because: <span><tr> - runtime typecheking happens every time the function is called, so it has a runtime overhead<!-- .element: class="fragment" data-fragment-index="1" --></tr></span> <span><tr> - our program can terminate without producing the correct output<!-- .element: class="fragment" data-fragment-index="2" --></tr></span> --- ## Example: Sometimes `None` <span> Consider: <!-- .element: class="fragment" data-fragment-index="1" --> ```python def sometimesNone() -> Optional[int]: return randint(1, 10) if random() > 0.2 else None ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> Without typechecking: <!-- .element: class="fragment" data-fragment-index="2" --> ```python def foo(a): return a * sometimesNone() ``` <!-- .element: class="fragment" data-fragment-index="2" --> <span> With typechecking: <!-- .element: class="fragment" data-fragment-index="3" --> ```python def foo(a: int) -> int: if (b := sometimesNone()) is not None: return a * b else: return a ``` <!-- .element: class="fragment" data-fragment-index="3" --> </span> ---- <span><tr> - Without typechecking you can merrily ignore the case when the result of `sometimesNone` is `None`, and get a `TypeError` at runtime. </tr></span> <span><tr> - With typechecking you are forced to do _something_ in that case. An alternative to the code presented above would be to pass through that `None`. <span><small>Smells like a monad.<!-- .element: class="fragment" data-fragment-index="3" --></small></span> <!-- .element: class="fragment" data-fragment-index="1" --> </tr></span> <span> ```python def foo(a: int) -> Optional[int]: if (b := sometimesNone()) is not None: return a * b else: return b ``` <!-- .element: class="fragment" data-fragment-index="2" --> </span> --- ## No More Null Pointers Rust uses static typechecking to prevent - dereferencing null pointers - data races - double frees - etc. --- You can have multiple immutable pointers to a piece of memory: <span> ```rust= let arr = [1, 2, 3]; let ptr1 = &arr; let ptr2 = &arr; ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span>But you can only have one mutable pointer to it:<!-- .element: class="fragment" data-fragment-index="2" --> </span> <span> ```rust=2 let ptr3 = &mut arr; // this is okay let ptr4 = &arr; // not okay, ptr1 is live let ptr5 = &mut arr; // also not okay ptr1[2] = 4; // necessary to keep ptr1 live ``` <!-- .element: class="fragment" data-fragment-index="3" --> </span> ---- How does the compiler keep track of what is mutable and what is not? Types! ```rust ptr1, ptr2, ptr4: &[i32]; ptr3, ptr5: &mut [i32]; ``` <span> The Rust type system is expressive enough to mark the mutability of variables. <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> In order to do the same in C++, you would need reference-counted pointers which add runtime overhead. <!-- .element: class="fragment" data-fragment-index="2" --> </span> --- ### What might go wrong <span> The following is legal C++, it compiles: ```cpp std::vector<int>* arr = new std::vector<int>({1, 2, 3}); std::vector<int>* ptr = arr; delete arr; std::cout << (*ptr)[0] << std::endl; ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> At runtime it causes 2020 to happen. <!-- .element: class="fragment" data-fragment-index="2" --> </span> <span> The equivalent Rust code does not compile: <!-- .element: class="fragment" data-fragment-index="3" --> </span> <span> ```rust=2 let mut arr = vec![1, 2, 3]; let ptr = &arr; Vec::drain(&mut arr, ..); println!("{:?}", ptr); ``` <!-- .element: class="fragment" data-fragment-index="3" --> </span> ---- We create a reference `ptr` to `arr` on line 3, then we create a mutable reference on line 4, and then we try to use `ptr` again on line 5. That is a problem, since `ptr` is no longer pointing to valid memory. ![](https://i.imgur.com/PQIRWC9.png) <!-- ![](https://i.imgur.com/v6za5fG.png) --> <!-- ![](https://i.imgur.com/6bPPYoT.png) --> <!-- ![](https://i.imgur.com/ZWthUzl.png) --> <!-- ![](https://i.imgur.com/5akyxLf.png) --> ---- - The `Vec<T>::drain` takes a mutable reference to a `Vec`, it has signature `drain(self: &mut Vec<T>, range: ...)`. <!-- .element: class="fragment" data-fragment-index="2" --></span> <span> - The compiler has an axiom that you can have either a single mutable reference or multiple immutable references to the same piece of memory. <!-- .element: class="fragment" data-fragment-index="4" --></span> <span> - But `ptr` is a live immutable reference to `arr` and `self` is a live mutable reference to `arr`, a contradiction. <!-- .element: class="fragment" data-fragment-index="5" --></span> ---- However, the following is valid ```rust=2 let mut arr: Vec<i32> = vec![1, 2, 3]; let ptr = &arr; drop(&mut arr); ``` since `ptr` gets optimized away by line 4 (it is not "live" at line 4). --- ## Optimizations Can Rust optimize the following code? ```rust fn compute(input: &u32, output: &mut u32) { if *input > 10 { *output = 1; } if *input > 5 { *output *= 2; } } ``` <span> Yes! <!-- .element: class="fragment" data-fragment-index="1" --> ```rust fn compute(input: &u32, output: &mut u32) { let cached = *input; // keep *input in a register if cached > 10 { *output = 2; // x > 10 implies x > 5, so double and exit } else if cached_input > 5 { *output *= 2; } } ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> ---- What about this C++ function? ```cpp void compute(int* input, int* output) { if (*input > 10) { *output = 1; } if (*input > 5) { *output *= 2; } } ``` <span> No! Consider this driver code: <!-- .element: class="fragment" data-fragment-index="1" --> ```cpp const int a = 20; const int* input = &a, *output = &a; printf("Before -> *input: %d, *output: %d\n", *input, *output); compute(input, output); printf("After -> *input: %d, *output: %d\n", *input, *output); ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> ``` Before -> *input: 20, *output: 20 After -> *input: 1, *output: 1 ``` <!-- .element: class="fragment" data-fragment-index="2" --> </span> ---- `const` __does not__ solves the problem in C++. Consider ```cpp void compute(const int* input, int* output) { if (*input > 10) { *output = 1; } if (*input > 5) { *output *= 2; } } ``` with driver code: ```cpp int a = 20; const int* input = &a; int* output = &a; // this is illegal in Rust printf("Before -> *input: %d, *output: %d\n", *input, *output); compute(input, output); printf("After -> *input: %d, *output: %d\n", *input, *output); ``` <span> ``` Before -> *input: 20, *output: 20 After -> *input: 1, *output: 1 ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> --- ## Example: Compile-time Length The following is valid Python, it compiles: ```python a = np.zeros((2, 3)) b = np.zeros((4, 5)) _ = a.dot(b) ``` But it causes a shape-mismatch error at runtime, since you cannot multiply a 2×3 matrix by a 4×5 matrix. <span> The following equivalent Rust code fails to compile: <!-- .element: class="fragment" data-fragment-index="1" --> ```rust let a = Matrix<f32, U2, U3, MatrixArray<f32, U2, U3>>::zero(); let b = Matrix<f32, U4, U5, MatrixArray<f32, U4, U5>>::zero(); let _ = a * b; ``` <!-- .element: class="fragment" data-fragment-index="1" --> </span> ---- Let's look at this closely. ```rust let a = Matrix<f32, U2, U3, MatrixArray<f32, U2, U3>>::zero(); ``` <span> - The `Matrix` and `MatrixArray` types are generic in their data type, number of rows, and number of columns. `Matrix` is also generic in its containter type, which is `MatrixArray<f32, U2, U3>` here. <!-- .element: class="fragment" data-fragment-index="1" --> </span> <span> - `U2`, `U3`, `U4`, and `U5` are type-level numbers. These numbers are evaluated at compile time, and get optimized away at runtime. <!-- .element: class="fragment" data-fragment-index="2" --> </span> ---- <span> This is the meat of the matrix multiplication implementation ([full](https://github.com/dimforge/nalgebra/blob/dev/src/base/ops.rs#L619)): <!-- .element: class="fragment" data-fragment-index="1" --> ```rust impl<...> Mul<Matrix<..., R2, C2, ...>> for Matrix<..., R1, C1, ...> where ShapeConstraint: AreMultipliable<R1, C1, R2, C2>, { type Output = MatrixMN<..., R1, C2>; fn mul(self, rhs: Matrix<..., R2, C2, ...>) -> Self::Output { &self * &rhs } } ``` <!-- .element: class="fragment" data-fragment-index="1" --> - Type constraints appear after `where`. Specifically, the constraint `AreMultipliable` ensures that only matrices of appropriate dimensions get multiplied together. <!-- .element: class="fragment" data-fragment-index="2" --> - It is a proposition, expressed as a type, which the compiler tries to prove, by finding an appropriate implementation! <!-- .element: class="fragment" data-fragment-index="3" --> </span> ---- This is the meat of the `AreMultipliable` type ([full](https://github.com/dimforge/nalgebra/blob/dev/src/base/constraint.rs)): ```rust struct ShapeConstraint; pub trait AreMultipliable<R1: Dim, C1: Dim, R2: Dim, C2: Dim>: DimEq<C1, R2> {} impl<R1: Dim, C1: Dim, R2: Dim, C2: Dim> AreMultipliable<R1, C1, R2, C2> for ShapeConstraint where ShapeConstraint: DimEq<C1, R2> {} pub trait DimEq<D1: Dim, D2: Dim> {} impl<D: Dim> DimEq<D, D> for ShapeConstraint {} ``` <span> - The compiler can produce a `DimEq<C1, R2>` instance for `ShapeConstraint` only if `C1` and `R2` are the same type (here, the same number). <!-- .element: class="fragment" data-fragment-index="1" --> - The empty bodies `{}` mean that these types cannot do anything at runtime, they will get optimized away. (In the actual code some of the bodies contain associated types, which also get optimized away.) <!-- .element: class="fragment" data-fragment-index="2" --> </span> ---- # Demo --- ## References, Further Reading - [Sierpiński space](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_space) - [Example of the Law of the Excluded Middle failing](https://math.stackexchange.com/a/1371237/783650) - [Curry-Howard isomorphism](https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspondence#Related_proofs-as-programs_correspondences) - [Aliasing in Rust](https://doc.rust-lang.org/nomicon/aliasing.html) - [typenum.rs](https://github.com/paholg/typenum) - [peano.rs](https://github.com/paholg/peano) - [Nalgebra](https://nalgebra.org/) - [Type-level rust in detail](https://beachape.com/blog/2017/03/12/gentle-intro-to-type-level-recursion-in-Rust-from-zero-to-frunk-hlist-sculpting/) - [More useful version of ^ but less comprehensive](https://beachape.com/blog/2017/03/12/gentle-intro-to-type-level-recursion-in-Rust-from-zero-to-frunk-hlist-sculpting/) - [Curry-Howard Isomorphism in Haskell (talk)](https://www.youtube.com/watch?v=tfG7T54MhIU&ab_channel=LambdaConf)
{"metaMigratedAt":"2023-06-15T14:01:46.240Z","metaMigratedFrom":"YAML","title":"Why (Good) Type Systems are Great","breaks":true,"contributors":"[{\"id\":\"26528808-5bdc-4353-af4a-57100a3fb803\",\"add\":32614,\"del\":11377}]"}
    613 views