Write some more
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Michael Zhang 2023-04-21 01:18:34 -05:00
parent 079c2a8b38
commit afaec7c0b6
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
3 changed files with 300 additions and 115 deletions

View file

@ -463,24 +463,31 @@ table.table {
min-width: 1px; min-width: 1px;
details { details {
border: 1px solid lightgray; border: 1px solid $hr-color;
// padding: 10px 30px; // padding: 10px 30px;
font-size: 0.9rem; font-size: 0.9rem;
padding: 0 30px; padding: 0 30px;
line-height: 1.5; line-height: 1.5;
p:nth-of-type(1) {
padding-top: 0;
margin-top: 0;
}
summary { summary {
padding: 10px 0; padding: 10px 0;
transition: margin 150ms ease-out;
} }
&[open] summary { &[open] summary {
border-bottom: 1px solid lightgray; border-bottom: 1px solid $hr-color;
margin-bottom: 15px;
} }
} }
hr { hr {
border-width: 1px 0 0 0; border-width: 1px 0 0 0;
border-color: $faded-background-color; border-color: $hr-color;
margin: 32px auto; margin: 32px auto;
width: 20%; width: 20%;
} }
@ -570,8 +577,8 @@ table.table {
} }
} }
.endline { hr.endline {
margin-top: 30px; margin-top: 30px;
border-width: 1px 0 0 0; border-width: 1px 0 0 0;
border-color: $faded-background-color; border-color: $hr-color;
} }

View file

@ -20,6 +20,7 @@ $monofont: "PragmataPro Mono Liga", "Roboto Mono", "Roboto Mono for Powerline",
$small-text-color: #6e707f; $small-text-color: #6e707f;
$smaller-text-color: lighten($text-color, 30%); $smaller-text-color: lighten($text-color, 30%);
$faded: lightgray; $faded: lightgray;
$hr-color: lightgray;
$link-color: royalblue; $link-color: royalblue;
$code-color: firebrick; $code-color: firebrick;
$tag-color: lighten($link-color, 35%); $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) { @media (prefers-color-scheme: dark) {
$background-color: #202030; $background-color: #202030;
$faded-background-color: lighten($background-color, 30%); $faded-background-color: lighten($background-color, 10%);
$shadow-color: lighten($background-color, 10%); $shadow-color: lighten($background-color, 10%);
$heading-color: lighten(lightskyblue, 20%); $heading-color: lighten(lightskyblue, 20%);
$text-color: #CDCDCD; $text-color: #CDCDCD;
$small-text-color: darken($text-color, 8%); $small-text-color: darken($text-color, 8%);
$smaller-text-color: darken($text-color, 12%); $smaller-text-color: darken($text-color, 12%);
$faded: #666; $faded: #666;
$hr-color: gray;
$link-color: lightskyblue; $link-color: lightskyblue;
$code-color: lighten(firebrick, 25%); $code-color: lighten(firebrick, 25%);
$tag-color: darken($link-color, 55%); $tag-color: darken($link-color, 55%);

View file

@ -12,27 +12,26 @@ entire proof system on top of a programming language and verify mathematical
statements purely with programs. statements purely with programs.
To make this idea more approachable, I'm going to stick to using [Typescript] 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 for some of the starting examples to explain the concepts. Then, we'll go and
way this language tackles a type system, and even though there are certain see how this stacks up in a language designed for writing mathematical proofs.
things it lacks, I still find it generally useful and intuitive for talking
about basic ideas.
--- ---
Let's start with a very simple data type: a class! We can make new classes in If we're going to talk about type theory, let's start by bringing some types to
Typescript like this: 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 ```ts
class Cake {} class Cake {}
``` ```
That's all there is to it! `Cake` is now a full-fledged data type. What does Simple enough. `Cake` is a type now. What does being a type mean?
being a data type mean?
- We can make some `Cake` like this: - We can make some `Cake` like this:
```ts ```ts
let cake = new Cake(); let cake1 = new Cake();
let cake2 = new Cake();
``` ```
- We can use it in function definitions, like this: - 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: - We can put it in other types, like this:
```ts ```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 ```ts
class NoCake { class NoCake {
@ -55,102 +58,122 @@ class NoCake {
} }
``` ```
In Typescript and many OO languages it's based on, classes are given > I admit, there still is a constructor, but just a private one, which means you
public constructors by default. This lets anyone create an instance of the > can't call it from outside the class. Same thing in effect.
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 In Typescript and many OO languages it's based on, classes are given public
anymore. 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.
<details> <details>
<summary>Why might we want to have private constructors?</summary> <summary>Why might we want to have private constructors?</summary>
Let's say you had integer user IDs, and in your application whenever you wrote 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 a helper function that had to deal with user IDs, you had to first check that
it was a valid ID: it was a valid ID:
```ts ```ts
function getUsername(id: number) { /** @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... // First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid 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 This is one way to do it. But throwing exceptions from constructors is
we know that every instance of this class was a valid id, rather than passing typically bad practice. This is because constructors are meant to be the final
around numbers and doing the check every time. 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 ```ts
function getUsername(id: UserId) { class UserId {
// We can just skip the first part entirely now! private id: number;
// Because of the type of the input, we are sure that id is valid. constructor(id: number) {
... this.id = id;
} }
```
You could probably imagine what an implementation of this type looks like: /** @throws {Error} */
fromNumberChecked() {
```ts // First, check if id is even a valid id...
class UserId { if (!isIdValid(id)) throw new Error("Invalid ID");
private id: number; return new UserId(id);
/** @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;
}
} }
``` }
```
This is one way to do it. But throwing exceptions from constructors is But this doesn't work if the constructor is also public, because someone can
typically bad practice. This is because constructors are meant to be the final just **bypass** this check function and call the constructor anyway with an
step in creating a particular object and should always return successfully and invalid id! We need to limit the constructor, so that all attempts to create
not have side effects. So in reality, what we want is to put this into a a `UserId` instance goes through our function:
_static method_ that can create `UserId` objects:
```ts ```ts
class UserId { class UserId {
private id: number; private id: number;
constructor(id: number) { this.id = id; } 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);
}
} }
```
But this doesn't work if the constructor is also public, because someone can /** @throws {Error} */
just **bypass** this check function and call the constructor anyway with an fromNumberChecked() {
invalid id! We need to limit the constructor, so that all attempts to create // First, check if id is even a valid id...
a `UserId` instance goes through our function: 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; }
/** @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.
</details> </details>
Let's keep going down this `NoCake` example, and suppose we never implemented 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? 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 ```ts
let cake = new NoCake(); let cake = new NoCake();
@ -175,7 +198,7 @@ But what about our use cases?
```ts ```ts
function giveMeCake(cake: NoCake) { ... } function giveMeCake(cake: NoCake) { ... }
type Party = { cake: NoCake, balloons: Balloons, ... }; type Party = { cake: NoCake, balloons: Balloon[], ... };
``` ```
Interestingly, this actually still typechecks! Why is that? 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 later on. Think about what happens to `Party` if one of its required elements
is something that can never be created. 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 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_, **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 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 instance of a bottom type, it can never be called, and if a type constructor
requires a bottom type, it can never be constructed. requires a bottom type, like `Party`, 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`.
Typescript actually has the concept of the bottom type baked into the language: 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. [`never`][never]. So we never actually needed to create this `NoCake` type at
For example, consider the following functions: 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 ```ts
// Throw an error // Throw an error
@ -224,7 +238,9 @@ function never1(): never {
// Infinite loop // Infinite loop
function never2(): never { 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 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 program will never reach the log statement, or any of the statements after it.
up and throw up before you get a chance to use the value after it, and the The `throw` will bubble up and throw up before you get a chance to use the value
infinite loop will never even finish, so nothing after it will get run. 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<T>(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<T>(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 [proposition]: https://en.wikipedia.org/wiki/Propositional_calculus
[typescript]: https://www.typescriptlang.org/ [typescript]: https://www.typescriptlang.org/
[never]: https://www.typescriptlang.org/docs/handbook/2/functions.html#never [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