diff --git a/assets/sass/_content.scss b/assets/sass/_content.scss
index 126d000..5baeaa0 100644
--- a/assets/sass/_content.scss
+++ b/assets/sass/_content.scss
@@ -463,24 +463,31 @@ table.table {
min-width: 1px;
details {
- border: 1px solid lightgray;
+ border: 1px solid $hr-color;
// padding: 10px 30px;
font-size: 0.9rem;
padding: 0 30px;
line-height: 1.5;
+ p:nth-of-type(1) {
+ padding-top: 0;
+ margin-top: 0;
+ }
+
summary {
padding: 10px 0;
+ transition: margin 150ms ease-out;
}
&[open] summary {
- border-bottom: 1px solid lightgray;
+ border-bottom: 1px solid $hr-color;
+ margin-bottom: 15px;
}
}
hr {
border-width: 1px 0 0 0;
- border-color: $faded-background-color;
+ border-color: $hr-color;
margin: 32px auto;
width: 20%;
}
@@ -570,8 +577,8 @@ table.table {
}
}
-.endline {
+hr.endline {
margin-top: 30px;
border-width: 1px 0 0 0;
- border-color: $faded-background-color;
+ border-color: $hr-color;
}
diff --git a/assets/sass/main.scss b/assets/sass/main.scss
index 1c547d7..0a72e2d 100644
--- a/assets/sass/main.scss
+++ b/assets/sass/main.scss
@@ -20,6 +20,7 @@ $monofont: "PragmataPro Mono Liga", "Roboto Mono", "Roboto Mono for Powerline",
$small-text-color: #6e707f;
$smaller-text-color: lighten($text-color, 30%);
$faded: lightgray;
+ $hr-color: lightgray;
$link-color: royalblue;
$code-color: firebrick;
$tag-color: lighten($link-color, 35%);
@@ -29,13 +30,14 @@ $monofont: "PragmataPro Mono Liga", "Roboto Mono", "Roboto Mono for Powerline",
@media (prefers-color-scheme: dark) {
$background-color: #202030;
- $faded-background-color: lighten($background-color, 30%);
+ $faded-background-color: lighten($background-color, 10%);
$shadow-color: lighten($background-color, 10%);
$heading-color: lighten(lightskyblue, 20%);
$text-color: #CDCDCD;
$small-text-color: darken($text-color, 8%);
$smaller-text-color: darken($text-color, 12%);
$faded: #666;
+ $hr-color: gray;
$link-color: lightskyblue;
$code-color: lighten(firebrick, 25%);
$tag-color: darken($link-color, 55%);
diff --git a/content/posts/2023-04-20-programmable-proofs.md b/content/posts/2023-04-20-programmable-proofs.md
index 3055971..4c0dbf9 100644
--- a/content/posts/2023-04-20-programmable-proofs.md
+++ b/content/posts/2023-04-20-programmable-proofs.md
@@ -12,27 +12,26 @@ entire proof system on top of a programming language and verify mathematical
statements purely with programs.
To make this idea more approachable, I'm going to stick to using [Typescript]
-for the majority of my examples, to explain the concepts. I personally love the
-way this language tackles a type system, and even though there are certain
-things it lacks, I still find it generally useful and intuitive for talking
-about basic ideas.
+for some of the starting examples to explain the concepts. Then, we'll go and
+see how this stacks up in a language designed for writing mathematical proofs.
---
-Let's start with a very simple data type: a class! We can make new classes in
-Typescript like this:
+If we're going to talk about type theory, let's start by bringing some types to
+the table. In Typescript, there's a very simple way to create a type: defining a
+class! We can make new classes in Typescript like this:
```ts
class Cake {}
```
-That's all there is to it! `Cake` is now a full-fledged data type. What does
-being a data type mean?
+Simple enough. `Cake` is a type now. What does being a type mean?
- We can make some `Cake` like this:
```ts
- let cake = new Cake();
+ let cake1 = new Cake();
+ let cake2 = new Cake();
```
- We can use it in function definitions, like this:
@@ -44,10 +43,14 @@ being a data type mean?
- We can put it in other types, like this:
```ts
- type Party = { cake: Cake, balloons: Balloons, ... };
+ type Party = { cake: Cake, balloons: Balloon[], ... };
```
-Ok that's cool. But what about classes with no constructors?
+ This creates a `Party` object that contains a `Cake`, some `Balloon`s, and
+ possibly some other things.
+
+Ok that's cool. But we're curious people, so suppose we decide to have some fun
+and make a class that doesn't have a constructor:
```ts
class NoCake {
@@ -55,102 +58,122 @@ class NoCake {
}
```
-In Typescript and many OO languages it's based on, classes are given
-public constructors by default. This lets anyone create an instance of the
-class, and get all the great benefits we just talked about above. But when we
-lock down the constructor, we can't just freely create instances of this class
-anymore.
+> I admit, there still is a constructor, but just a private one, which means you
+> can't call it from outside the class. Same thing in effect.
+
+In Typescript and many OO languages it's based on, classes are given public
+constructors by default. This lets anyone create an instance of the class, and
+get all the great benefits we just talked about above. But when we lock down the
+constructor, we can't just freely create instances of this class anymore.
Why might we want to have private constructors?
- Let's say you had integer user IDs, and in your application whenever you wrote
- a helper function that had to deal with user IDs, you had to first check that
- it was a valid ID:
+Let's say you had integer user IDs, and in your application whenever you wrote
+a helper function that had to deal with user IDs, you had to first check that
+it was a valid ID:
- ```ts
- function getUsername(id: number) {
+```ts
+/** @throws {Error} */
+function getUsername(id: number): string {
+ // First, check if id is even a valid id...
+ if (!isIdValid(id)) throw new Error("Invalid ID");
+
+ // Ok, now we are sure that id is valid.
+ ...
+}
+```
+
+It would be really nice if we could make a type to encapsulate this, so that
+we know that every instance of this class was a valid id, rather than passing
+around numbers and doing the check every time.
+
+```ts
+function getUsername(id: UserId): string {
+ // We can just skip the first part entirely now!
+ // Because of the type of the input, we are sure that id is valid.
+ ...
+}
+```
+
+Note that the function also won't throw an exception for an invalid id
+anymore! That's because we're moving the check somewhere else. You could
+probably imagine what an implementation of this "somewhere else" looks like:
+
+```ts
+class UserId {
+ private id: number;
+
+ /** @throws {Error} */
+ constructor(id: number) {
// First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid ID");
- // Ok, now we are sure that id is valid.
- ...
+ this.id = number;
}
- ```
+}
+```
- It would be really nice if we could make a type to encapsulate this, so that
- we know that every instance of this class was a valid id, rather than passing
- around numbers and doing the check every time.
+This is one way to do it. But throwing exceptions from constructors is
+typically bad practice. This is because constructors are meant to be the final
+step in creating a particular object and should always return successfully and
+not have side effects. So in reality, what we want is to put this into a
+_static method_ that can create `UserId` objects:
- ```ts
- function getUsername(id: UserId) {
- // We can just skip the first part entirely now!
- // Because of the type of the input, we are sure that id is valid.
- ...
+```ts
+class UserId {
+ private id: number;
+ constructor(id: number) {
+ this.id = id;
}
- ```
- You could probably imagine what an implementation of this type looks like:
-
- ```ts
- class UserId {
- private id: number;
-
- /** @throws {Error} */
- constructor(id: number) {
- // First, check if id is even a valid id...
- if (!isIdValid(id)) throw new Error("Invalid ID");
-
- this.id = number;
- }
+ /** @throws {Error} */
+ fromNumberChecked() {
+ // First, check if id is even a valid id...
+ if (!isIdValid(id)) throw new Error("Invalid ID");
+ return new UserId(id);
}
- ```
+}
+```
- This is one way to do it. But throwing exceptions from constructors is
- typically bad practice. This is because constructors are meant to be the final
- step in creating a particular object and should always return successfully and
- not have side effects. So in reality, what we want is to put this into a
- _static method_ that can create `UserId` objects:
+But this doesn't work if the constructor is also public, because someone can
+just **bypass** this check function and call the constructor anyway with an
+invalid id! We need to limit the constructor, so that all attempts to create
+a `UserId` instance goes through our function:
- ```ts
- class UserId {
- private id: number;
- constructor(id: number) { this.id = id; }
-
- /** @throws {Error} */
- fromNumberChecked() {
- // First, check if id is even a valid id...
- if (!isIdValid(id)) throw new Error("Invalid ID");
- return new UserId(id);
- }
+```ts
+class UserId {
+ private id: number;
+ private constructor(id: number) {
+ this.id = id;
}
- ```
- But this doesn't work if the constructor is also public, because someone can
- just **bypass** this check function and call the constructor anyway with an
- invalid id! We need to limit the constructor, so that all attempts to create
- a `UserId` instance goes through our function:
-
- ```ts
- class UserId {
- private id: number;
- private constructor(id: number) { this.id = id; }
-
- /** @throws {Error} */
- fromNumberChecked() {
- // First, check if id is even a valid id...
- if (!isIdValid(id)) throw new Error("Invalid ID");
- return new UserId(id);
- }
+ /** @throws {Error} */
+ fromNumberChecked() {
+ // First, check if id is even a valid id...
+ if (!isIdValid(id)) throw new Error("Invalid ID");
+ return new UserId(id);
}
- ```
+}
+```
+
+Now we can rest assured that no matter what, if I get passed a `UserId`
+object, it's going to be valid.
+
+```ts
+function getUsername(id: UserId): string {
+ // We can just skip the first part entirely now!
+ // Because of the type of the input, we are sure that id is valid.
+ ...
+}
+```
+
+This works for all kinds of validations, as long as the validation is
+_permanent_. So actually in our example, a user could get deleted while we
+still have `UserId` objects lying around, so the validity would not be true.
+But if we've checked that an email is of the correct form, for example, then
+we know it's valid forever.
- Now we can rest assured that no matter what, if I get passed a `UserId`
- object, it's going to be valid. This works for all kinds of validations, as
- long as the validation is _permanent_. So actually in our example, a user
- could get deleted while we still have `UserId` objects lying around, so the
- validity would not be true. But if we've checked that an email is of the
- correct form, for example, then we know it's valid forever.
Let's keep going down this `NoCake` example, and suppose we never implemented
@@ -160,7 +183,7 @@ create an instance of this class_.
But what about our use cases?
-- We can no longer make `NoCake`. This produces a code visibility error:
+- We can no longer make `NoCake`. This produces a method visibility error:
```ts
let cake = new NoCake();
@@ -175,7 +198,7 @@ But what about our use cases?
```ts
function giveMeCake(cake: NoCake) { ... }
- type Party = { cake: NoCake, balloons: Balloons, ... };
+ type Party = { cake: NoCake, balloons: Balloon[], ... };
```
Interestingly, this actually still typechecks! Why is that?
@@ -189,7 +212,9 @@ But what about our use cases?
later on. Think about what happens to `Party` if one of its required elements
is something that can never be created.
- No cake, no party! The `Party` type _also_ can't ever be constructed.
+ No cake, no party! The `Party` type _also_ can't ever be constructed. The
+ _type_ called `Party` still exists, we just can't produce an object that will
+ qualify as a `Party`.
We've actually created a very important concept in type theory, known as the
**bottom type**. In math, this corresponds to $\emptyset$, or the _empty set_,
@@ -198,23 +223,12 @@ but in logic and type theory we typically write it as $\bot$ and read it as
What this represents is a concept of impossibility. If a function requires an
instance of a bottom type, it can never be called, and if a type constructor
-requires a bottom type, it can never be constructed.
-
-> Aside: in type theory, type constructors like structs and generics are really
-> just Type $\rightarrow$ Type functions, so you could also think of it as a
-> function as well. The difference is that the type constructor can actually
-> succeed. I can write this type:
->
-> ```ts
-> type NoCakeList = NoCake[];
-> ```
->
-> But because I can never get an instance of `NoCake`, I can also never get an
-> instance of `NoCakeList`.
+requires a bottom type, like `Party`, it can never be constructed.
Typescript actually has the concept of the bottom type baked into the language:
-[`never`][never]. It's used to describe computations that never produce a value.
-For example, consider the following functions:
+[`never`][never]. So we never actually needed to create this `NoCake` type at
+all! We could've just used `never`. It's used to describe computations that
+never produce a value. For example, consider the following functions:
```ts
// Throw an error
@@ -224,7 +238,9 @@ function never1(): never {
// Infinite loop
function never2(): never {
- while (true) { }
+ while (true) {
+ /* no breaks */
+ }
}
```
@@ -239,10 +255,170 @@ function usingNever() {
```
You can _never_ assign a value to the variable `canThisValueExist`, because the
-program will never reach any of the statements after it. The `throw` will bubble
-up and throw up before you get a chance to use the value after it, and the
-infinite loop will never even finish, so nothing after it will get run.
+program will never reach the log statement, or any of the statements after it.
+The `throw` will bubble up and throw up before you get a chance to use the value
+after it, and the infinite loop will never even finish, so nothing after it will
+ever get run.
+
+We can actually confirm that the `never` type can represent logical falsehood,
+by using one of the "features" of false, which is that ["false implies
+anything"][false]. In code, this looks like:
+
+```ts
+function exFalso(x: never): T {
+ return x;
+}
+```
+
+You'll notice that even though this function returns `T`, you can just return
+`x` and the typechecker is satisfied with it. Typescript bakes this behavior
+into the subtyping rules of `never`, so `never` type-checks as any other type,
+so for example you can't do this easily with our `NoCake` type.
+
+```ts
+// Does not type-check!
+function notExFalso(x: NoCake): T {
+ return x;
+}
+```
+
+Ok, we've covered bottom types, and established that it's analogous to logical
+falsehood. But what represents logical truth?
+
+Well, here's where we have to talk about what truth means. There's two big
+parties of logical thought that are involved here. Let's meet them:
+
+- **Classical logic**: gets its name from distinguishing itself from
+ intuitionistic logic. In this logical system, we can make statements and
+ talk about their truth value. As long as we've created a series of logical
+ arguments from the axioms at the start to get to "true", we're all good.
+ Under this framework, we can talk about things without knowing what they
+ are, so we can prove things like "given many non-empty sets, we can choose one
+ item out of each set" without actually being able to name any items or a way
+ to pick items (see Axiom of Choice)
+
+- **Intuitionistic logic**, also called **constructive logic**: the new kid on
+ the block. In intuitionistic logic, we need to find a _witness_ for all of
+ our proofs. Also, falsehood is the absence of a witness, just like all the
+ stuff with the bottom type we just talked about, which models impossibility or
+ absurdity. In this logical system, the Axiom of Choice doesn't make any sense:
+ the act of proving that a choice exists involves generating an example of some
+ sort. Another notable difference is that intuitionistic does not consider the
+ Law of Excluded Middle to be true.
+
+However, intuitionistic logic is preferred in a lot of mechanized theorem
+provers, because it's easier computationally to talk about "having a witness".
+Let's actually look more closely at what it means to have a "witness".
+
+If you didn't read the section earlier about using private constructors to
+restrict type construction, I'm going to revisit the idea a bit here. Consider
+this very restrictive hash map:
+
+```ts
+namespace HashMap {
+ class Ticket { ... } // Private to namespace
+
+ export class HashMap {
+ ...
+ public insert(k: string, v: number): Ticket { ... }
+
+ public getWithoutTicket(k: string): number | null {
+ if (!this.hasKey(k)) return null;
+ return this.inner[k];
+ }
+
+ public getWithTicket(t: Ticket): number {
+ return this.inner[t.key];
+ }
+ }
+}
+```
+
+The idea behind this is kind of like having a witness. Whenever you insert
+something into the map, you get back a ticket that basically proves that it was
+the one that inserted it for you in the first place.
+
+Since we used a namespace to hide access to the `Ticket` class, the only way you
+could have constructed a ticket is by inserting into the map. The ticket then
+serves as a _proof_ of the existence of the element inside the map (assuming you
+can't remove items).
+
+Now that we have a proof, we don't need to have `number | null` and check at
+runtime if a key was a valid key or not. As long as we have the ticket, it
+_guarantees_ for us that that key exists and we can just fetch it without asking
+any questions.
+
+In mechanized theorem provers,
+
+---
+
+To talk about that, we have to go back to treating types as sets. For example,
+we've already seen that the bottom type is a set with no elements, $\emptyset$.
+But what's a type that corresponds to a set with one element?
+
+In type theory, we call this a **unit type**. It's only got one element in it,
+so it's not super interesting. But that makes it a great return value for
+functions that aren't really returning anything, but _do_ return at all, as
+opposed to the bottom types. Typescript has something that behaves similarly:
+
+```ts
+function returnUnit(): null {
+ return null;
+}
+```
+
+There's nothing else that fits in this type. (I realize `void` and `undefined`
+also exist and are basically single-valued types, but they have other weird
+behavior that makes me not want to lump them in here)
+
+Let's just also jump into two-valued types, which in math we just call
+$\mathbf{2}$. This type has a more common analog in programming, it's the
+boolean type.
+
+Do you see how the number of possible values in a type starts to relate to the
+_size_ of the set now? I could go on, but typically 3+ sets aren't common enough
+to warrant languages providing built-in names for them. Instead, what languages
+will do is offer _unions_, which allow you to create your own structures.
+
+For example, let's try to create a type with 3 possible values. We're going to
+need 3 different unit types such that we can write something like this:
+
+```ts
+type Three = First | Second | Third;
+```
+
+We can just do three different unit classes.
+
+```ts
+class First {
+ static instance: First = new First();
+ private constructor() {}
+}
+class Second { ... } // same thing
+class Third { ... } // same thing
+```
+
+The reason I slapped on a private constructor and included a singleton
+`instance` variable is to ensure that there's only one value of the type that
+exists. Otherwise you could do something like this:
+
+```ts
+let first = new First();
+let differentFirst = new First();
+```
+
+and the size of the `First` type would have grown beyond just a single unit
+element.
+
+But anyway! You could repeat this kind of formulation for any number of
+_finitely_ sized types. In the biz, we would call the operator `|` a
+**type-former**, since it just took three different types and created a new type
+that used all three of them.
[proposition]: https://en.wikipedia.org/wiki/Propositional_calculus
[typescript]: https://www.typescriptlang.org/
[never]: https://www.typescriptlang.org/docs/handbook/2/functions.html#never
+[false]: https://en.wikipedia.org/wiki/Principle_of_explosion
+[as]: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
+[//]: https://code.lol/post/programming/higher-kinded-types/
+[//]: https://ayazhafiz.com/articles/21/typescript-type-system-lambda-calculus