new blog post
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Michael Zhang 2024-06-28 20:48:21 -05:00
parent 531b33442d
commit 043b3ebe74
5 changed files with 357 additions and 60 deletions

View file

@ -8,6 +8,7 @@ import {
mkdirSync, mkdirSync,
mkdtempSync, mkdtempSync,
readFileSync, readFileSync,
existsSync,
readdirSync, readdirSync,
copyFileSync, copyFileSync,
writeFileSync, writeFileSync,
@ -35,6 +36,8 @@ const remarkAgda: RemarkPlugin = ({ base, publicDir }: Options) => {
const tempDir = mkdtempSync(join(tmpdir(), "agdaRender.")); const tempDir = mkdtempSync(join(tmpdir(), "agdaRender."));
const agdaOutDir = join(tempDir, "output"); const agdaOutDir = join(tempDir, "output");
const agdaOutFilename = parse(path).base.replace(/\.lagda.md/, ".md");
const agdaOutFile = join(agdaOutDir, agdaOutFilename);
mkdirSync(agdaOutDir, { recursive: true }); mkdirSync(agdaOutDir, { recursive: true });
const childOutput = spawnSync( const childOutput = spawnSync(
@ -44,18 +47,32 @@ const remarkAgda: RemarkPlugin = ({ base, publicDir }: Options) => {
`--html-dir=${agdaOutDir}`, `--html-dir=${agdaOutDir}`,
"--highlight-occurrences", "--highlight-occurrences",
"--html-highlight=code", "--html-highlight=code",
"--allow-unsolved-metas",
path, path,
], ],
{}, {},
); );
// TODO: Handle child output if (childOutput.error || !existsSync(agdaOutFile)) {
console.error("--AGDA OUTPUT--"); throw new Error(
console.error(childOutput); `Agda error:
console.error(childOutput.stdout?.toString());
console.error(childOutput.stderr?.toString()); Stdout:
console.error("--AGDA OUTPUT--"); ${childOutput.stdout}
Stderr:
${childOutput.stderr}`,
{
cause: childOutput.error,
},
);
}
// // TODO: Handle child output
// console.error("--AGDA OUTPUT--");
// console.error(childOutput);
// console.error(childOutput.stdout?.toString());
// console.error(childOutput.stderr?.toString());
// console.error("--AGDA OUTPUT--");
const referencedFiles = new Set(); const referencedFiles = new Set();
for (const file of readdirSync(agdaOutDir)) { for (const file of readdirSync(agdaOutDir)) {
@ -87,11 +104,9 @@ const remarkAgda: RemarkPlugin = ({ base, publicDir }: Options) => {
} }
} }
const filename = parse(path).base.replace(/\.lagda.md/, ".md");
const htmlname = parse(path).base.replace(/\.lagda.md/, ".html"); const htmlname = parse(path).base.replace(/\.lagda.md/, ".html");
const fullOutputPath = join(agdaOutDir, filename);
const doc = readFileSync(fullOutputPath); const doc = readFileSync(agdaOutFile);
// This is the post-processed markdown with HTML code blocks replacing the Agda code blocks // This is the post-processed markdown with HTML code blocks replacing the Agda code blocks
const tree2 = fromMarkdown(doc); const tree2 = fromMarkdown(doc);

View file

@ -4,22 +4,24 @@ module Prelude where
open import Agda.Primitive open import Agda.Primitive
module 𝟘 where private
data : Set where variable
¬_ : Set Set l : Level
¬ A = A
open 𝟘 public
module 𝟙 where data : Set where
data : Set where
rec-⊥ : {A : Set} A
rec-⊥ ()
¬_ : Set Set
¬ A = A
data : Set where
tt : tt :
open 𝟙 public
module 𝟚 where data Bool : Set where
data Bool : Set where
true : Bool true : Bool
false : Bool false : Bool
open 𝟚 public
id : {l : Level} {A : Set l} A A id : {l : Level} {A : Set l} A A
id x = x id x = x
@ -39,6 +41,19 @@ open Nat public
infix 4 _≡_ infix 4 _≡_
data _≡_ {l} {A : Set l} : (a b : A) Set l where data _≡_ {l} {A : Set l} : (a b : A) Set l where
instance refl : {x : A} x x instance refl : {x : A} x x
{-# BUILTIN EQUALITY _≡_ #-}
ap : {A B : Set l} (f : A B) {x y : A} x y f x f y
ap f refl = refl
sym : {A : Set l} {x y : A} x y y x
sym refl = refl
trans : {A : Set l} {x y z : A} x y y z x z
trans refl refl = refl
infixl 10 _∙_
_∙_ = trans
transport : {l₁ l₂ : Level} {A : Set l₁} {x y : A} transport : {l₁ l₂ : Level} {A : Set l₁} {x y : A}
(P : A Set l₂) (P : A Set l₂)
@ -72,3 +87,9 @@ _∘_ : {A B C : Set} (g : B → C) → (f : A → B) → A → C
__ : {A B : Set} (f g : A B) Set __ : {A B : Set} (f g : A B) Set
__ {A} f g = (x : A) f x g x __ {A} f g = (x : A) f x g x
postulate
funExt : {l : Level} {A B : Set l}
{f g : A B}
((x : A) f x g x)
f g

View file

@ -1,5 +1,5 @@
--- ---
title: "Formally proving true ≢ false in cubical Agda" title: "Formally proving true ≢ false in Homotopy Type Theory with Agda"
slug: "proving-true-from-false" slug: "proving-true-from-false"
date: 2023-04-21 date: 2023-04-21
tags: ["type-theory", "agda"] tags: ["type-theory", "agda"]
@ -12,12 +12,7 @@ math: true
These are some imports that are required for code on this page to work properly. These are some imports that are required for code on this page to work properly.
```agda ```agda
{-# OPTIONS --cubical #-} open import Prelude
open import Cubical.Foundations.Prelude
open import Prelude hiding (_≢_; _≡_; refl; transport)
infix 4 _≢_
_≢_ : ∀ {A : Set} → A → A → Set
x ≢ y = ¬ (x ≡ y)
``` ```
</details> </details>
@ -53,9 +48,8 @@ left side so it becomes judgmentally equal to the right:
- suc (suc (suc zero)) - suc (suc (suc zero))
- 3 - 3
However, in cubical Agda, naively using `refl` with the inverse statement However, in Agda, naively using `refl` with the inverse statement doesn't work.
doesn't work. I've commented it out so the code on this page can continue to I've commented it out so the code on this page can continue to compile.
compile.
``` ```
-- true≢false = refl -- true≢false = refl
@ -88,7 +82,7 @@ The strategy here is we define some kind of "type-map". Every time we see
`false`, we'll map it to empty. `false`, we'll map it to empty.
``` ```
bool-map : Bool → Type bool-map : Bool → Set
bool-map true = bool-map true =
bool-map false = ⊥ bool-map false = ⊥
``` ```
@ -98,26 +92,29 @@ over a path (the path supposedly given to us as the witness that true ≢ false)
will produce a function from the inhabited type we chose to the empty type! will produce a function from the inhabited type we chose to the empty type!
``` ```
true≢false p = transport (λ i → bool-map (p i)) tt true≢false p = transport bool-map p tt
``` ```
I used `true` here, but I could equally have just used anything else: I used `true` here, but I could equally have just used anything else:
``` ```
bool-map2 : Bool → Type bool-map2 : Bool → Set
bool-map2 true = 1 ≡ 1 bool-map2 true = 1 ≡ 1
bool-map2 false = ⊥ bool-map2 false = ⊥
true≢false2 : true ≢ false true≢false2 : true ≢ false
true≢false2 p = transport (λ i → bool-map2 (p i)) refl true≢false2 p = transport bool-map2 p refl
``` ```
## Note on proving divergence on equivalent values ## Note on proving divergence on equivalent values
Let's make sure this isn't broken by trying to apply this to something that's > [!NOTE]
actually true: > Update: some of these have been commented out since regular Agda doesn't support higher inductive types
``` Let's make sure this isn't broken by trying to apply this to something that's
actually true, like this higher inductive type:
```text
data NotBool : Type where data NotBool : Type where
true1 : NotBool true1 : NotBool
true2 : NotBool true2 : NotBool
@ -128,8 +125,7 @@ In this data type, we have a path over `true1` and `true2` that is a part of the
definition of the `NotBool` type. Since this is an intrinsic equality, we can't definition of the `NotBool` type. Since this is an intrinsic equality, we can't
map `true1` and `true2` to divergent types. Let's see what happens: map `true1` and `true2` to divergent types. Let's see what happens:
``` ```text
{-# NON_COVERING #-}
notbool-map : NotBool → Type notbool-map : NotBool → Type
notbool-map true1 = notbool-map true1 =
notbool-map true2 = ⊥ notbool-map true2 = ⊥

View file

@ -12,7 +12,7 @@ First off, a demonstration. Here's the code for function application over a path
``` ```
open import Agda.Primitive open import Agda.Primitive
open import Prelude open import Prelude hiding (ap)
ap : {l1 l2 : Level} {A : Set l1} {B : Set l2} {x y : A} ap : {l1 l2 : Level} {A : Set l1} {B : Set l2} {x y : A}
→ (f : A → B) → (f : A → B)

View file

@ -1,5 +1,5 @@
--- ---
title: "Boolean equivalences" title: "Boolean equivalences in HoTT"
slug: 2024-06-28-boolean-equivalences slug: 2024-06-28-boolean-equivalences
date: 2024-06-28T21:37:04.299Z date: 2024-06-28T21:37:04.299Z
tags: ["agda", "type-theory", "hott"] tags: ["agda", "type-theory", "hott"]
@ -15,25 +15,41 @@ $$
(2 \simeq 2) \simeq 2 (2 \simeq 2) \simeq 2
$$ $$
This problem is exercise 2.13 of the [Homotopy Type Theory book][book]. If you > [!NOTE]
are planning to attempt this problem, spoilers ahead! > This problem is exercise 2.13 of the [Homotopy Type Theory book][book]. If you
> are planning to attempt this problem, spoilers ahead!
[book]: https://homotopytypetheory.org/book/ [book]: https://homotopytypetheory.org/book/
<small>The following line imports some of the definitions used in this post.</small> <small>The following line imports some of the definitions used in this post.</small>
``` ```
{-# OPTIONS --allow-unsolved-metas #-}
open import Prelude open import Prelude
open Σ
``` ```
## The problem explained ## The problem explained
With just the notation, the problem may not be clear. With just that notation, the problem may not be clear.
$2$ represents the set with only 2 elements in it, which is the booleans. The $2$ represents the set with only two elements in it, which is the booleans. The
elements of $2$ are $\textrm{true}$ and $\textrm{false}$. elements of $2$ are $\textrm{true}$ and $\textrm{false}$.
In this expression, $\simeq$ denotes [_homotopy equivalence_][1] between sets,
which you can think of as an isomorphism. ```
For some function $f : A \rightarrow B$, we say that $f$ is an equivalence if: data 𝟚 : Set where
true : 𝟚
false : 𝟚
```
In the problem statement, $\simeq$ denotes [_homotopy equivalence_][1] between
sets, which you can think of as basically a fancy isomorphism. For some
function $f : A \rightarrow B$, we say that $f$ is an equivalence[^equivalence]
if:
[^equivalence]: There are other ways to define equivalences. As we'll show, an important
property that is missed by this definition is that equivalences should be _mere
propositions_. The reason why this definition falls short of that requirement is
shown by Theorem 4.1.3 in the [HoTT book][book].
[1]: https://en.wikipedia.org/wiki/Homotopy#Homotopy_equivalence [1]: https://en.wikipedia.org/wiki/Homotopy#Homotopy_equivalence
@ -56,7 +72,7 @@ Then, the expression $2 \simeq 2$ simply expresses an equivalence between the
boolean set to itself. Here's an example of a function that satisfies this equivalence: boolean set to itself. Here's an example of a function that satisfies this equivalence:
``` ```
bool-id : Bool → Bool bool-id : 𝟚𝟚
bool-id b = b bool-id b = b
``` ```
@ -74,9 +90,9 @@ bool-id-eqv = mkEquiv bool-id id-homotopy id-homotopy
We can package these together into a single definition that combines the We can package these together into a single definition that combines the
function along with proof of its equivalence properties using a dependent function along with proof of its equivalence properties using a dependent
pair[^1]: pair[^dependent-pair]:
[^1]: A dependent pair (or $\Sigma$-type) is like a regular pair $\langle x, y\rangle$, but where $y$ can depend on $x$. [^dependent-pair]: A dependent pair (or $\Sigma$-type) is like a regular pair $\langle x, y\rangle$, but where $y$ can depend on $x$.
For example, $\langle x , \textrm{isPrime}(x) \rangle$. For example, $\langle x , \textrm{isPrime}(x) \rangle$.
In this case it's useful since we can carry the equivalence information along with the function itself. In this case it's useful since we can carry the equivalence information along with the function itself.
This type is rather core to Martin-Löf Type Theory, you can read more about it [here][dependent-pair]. This type is rather core to Martin-Löf Type Theory, you can read more about it [here][dependent-pair].
@ -93,7 +109,7 @@ A ≃ B = Σ (A → B) (λ f → isEquiv f)
This gives us an equivalence: This gives us an equivalence:
``` ```
bool-eqv : Bool ≃ Bool bool-eqv : 𝟚𝟚
bool-eqv = bool-id , bool-id-eqv bool-eqv = bool-id , bool-id-eqv
``` ```
@ -106,7 +122,7 @@ equivalences between the boolean type and itself. Here is another possible
definition: definition:
``` ```
bool-neg : Bool → Bool bool-neg : 𝟚𝟚
bool-neg true = false bool-neg true = false
bool-neg false = true bool-neg false = true
``` ```
@ -127,7 +143,7 @@ bool-neg-eqv = mkEquiv bool-neg neg-homotopy neg-homotopy
neg-homotopy true = refl neg-homotopy true = refl
neg-homotopy false = refl neg-homotopy false = refl
bool-eqv2 : Bool ≃ Bool bool-eqv2 : 𝟚𝟚
bool-eqv2 = bool-neg , bool-neg-eqv bool-eqv2 = bool-neg , bool-neg-eqv
``` ```
@ -147,6 +163,255 @@ That's where the main exercise statement comes in: "show that" $(2 \simeq 2)
\simeq 2$, or in other words, find an inhabitant of this type. \simeq 2$, or in other words, find an inhabitant of this type.
``` ```
_ : (Bool ≃ Bool) ≃ Bool main-theorem : (𝟚𝟚) ≃ 𝟚
_ = {! !}
``` ```
How do we do this? Well, remember what it means to define an equivalence: first
define a function, then show that it is an equivalence. Since equivalences are
symmetrical, it doesn't matter which way we start first. I'll choose to first
define a function $2 \rightarrow 2 \simeq 2$. This is fairly easy to do by
case-splitting:
```
f : 𝟚 → (𝟚𝟚)
f true = bool-eqv
f false = bool-eqv2
```
This maps `true` to the equivalence derived from the identity function, and
`false` to the function derived from the negation function.
Now we need another function in the other direction. We can't case-split on
functions, but we can certainly case-split on their output. Specifically, we can
differentiate `id` from `neg` by their behavior when being called on `true`:
- $\textrm{id}(\textrm{true}) :\equiv \textrm{true}$
- $\textrm{neg}(\textrm{true}) :\equiv \textrm{false}$
```
g : (𝟚𝟚) → 𝟚
g eqv = (fst eqv) true
-- same as this:
-- let (f , f-is-equiv) = eqv in f true
```
Hold on. If you know about the cardinality of functions, you'll observe that in
the space of functions $f : 2 \rightarrow 2$, there are $2^2 = 4$ equivalence
classes of functions. So if we mapped 4 functions into 2, how could this be
considered an equivalence?
A **key observation** here is that we're not mapping from all possible functions
$f : 2 \rightarrow 2$. We're mapping functions $f : 2 \simeq 2$ that have
already been proven to be equivalences. This means we can count on them to be
structure-preserving, and can rule out cases like the function that maps
everything to true, since it can't possibly have an inverse.
We'll come back to this later.
First, let's show that $g \circ f \sim \textrm{id}$. This one is easy, we can
just case-split. Each of the cases reduces to something that is definitionally
equal, so we can use `refl`.
```
g∘f : (g ∘ f) id
g∘f true = refl
g∘f false = refl
```
Now comes the complicated case: proving $f \circ g \sim \textrm{id}$.
> [!NOTE]
> Since Agda's comment syntax is `--`, the horizontal lines in the code below
> are simply a visual way of separating out our proof premises from our proof
> goals.
```
module f∘g-case where
goal :
(eqv : 𝟚𝟚)
--------------------
→ f (g eqv) ≡ id eqv
f∘g : (f ∘ g) id
f∘g eqv = goal eqv
```
Now our goal is to show that for any equivalence $\textrm{eqv} : 2 \simeq 2$,
applying $f ∘ g$ to it is the same as not doing anything. We can evaluate the
$g(\textrm{eqv})$ a little bit to give us a more detailed goal:
```
goal2 :
(eqv : 𝟚𝟚)
--------------------------
→ f ((fst eqv) true) ≡ eqv
-- Solving goal with goal2, leaving us to prove goal2
goal eqv = goal2 eqv
```
The problem is if you think about equivalences as encoding some rich data about
a function, converting it to a boolean and back is sort of like shoving it into
a lower-resolution domain and then bringing it back. How can we prove that the
equivalence is still the same equivalence, and as a result proving that there
are only 2 possible equivalences?
Note that the function $f$ ultimately still only produces 2 values. That means
if we want to prove this, we can case-split on $f$'s output. In Agda, this uses
a syntax known as [with-abstraction]:
[with-abstraction]: https://agda.readthedocs.io/en/v2.6.4.3-r1/language/with-abstraction.html
```
goal3 :
(eqv : 𝟚𝟚) → (b : 𝟚) → (p : fst eqv true ≡ b)
------------------------------------------------
→ f b ≡ eqv
-- Solving goal2 with goal3, leaving us to prove goal3
goal2 eqv with b ← (fst eqv) true in p = goal3 eqv b p
```
We can now case-split on $b$, which is the output of calling $f$ on the
equivalence returned by $g$. This means that for the `true` case, we need to
show that $f(b) = \textrm{bool-eqv}$ (which is based on `id`) is equivalent to
the equivalence that generated the `true`.
Let's start with the `id` case; we just need to show that for every equivalence
$e$ where running the equivalence function on `true` also returned `true`, $e
\equiv f(\textrm{true})$.
Unfortunately, we don't know if this is true unless our equivalences are _mere
propositions_, meaning if two functions are identical, then so are their
equivalences.
$$
\textrm{isProp}(P) :\equiv \prod_{x, y: P}(x \equiv y)
$$
<small>Definition 3.3.1 from the [HoTT book][book]</small>
Applying this to $\textrm{isEquiv}(f)$, we get the property:
$$
\sum_{f : A → B} \left( \prod_{e_1, e_2 : \textrm{isEquiv}(f)} e_1 \equiv e_2 \right)
$$
This proof is shown later in the book, so I will use it here directly without proof[^equiv-isProp]:
[^equiv-isProp]: I haven't worked that far in the book yet, but this is shown in Theorem 4.2.13 in the [HoTT book][book].
I might write a separate post about that when I get there, it seems like an interesting proof as well!
```
postulate
equiv-isProp : {A B : Set}
→ (e1 e2 : A ≃ B) → (Σ.fst e1 ≡ Σ.fst e2)
-----------------------------------------
→ e1 ≡ e2
```
Now we're going to need our **key observation** that we made earlier, that
equivalences must not map both values to a single one.
This way, we can pin the behavior of the function on all inputs by just using
its behavior on `true`, since its output on `false` must be _different_.
We can use a proof that [$\textrm{true} \not\equiv \textrm{false}$][true-not-false] that I've shown previously.
[true-not-false]: https://mzhang.io/posts/proving-true-from-false/
```
true≢false : true ≢ false
-- read: true ≡ false → ⊥
true≢false p = bottom-generator p
where
map : 𝟚 → Set
map true =
map false = ⊥
bottom-generator : true ≡ false → ⊥
bottom-generator p = transport map p tt
```
Let's transform this into information about $f$'s output on different inputs:
```
observation : (f : 𝟚𝟚) → isEquiv f → f true ≢ f false
-- read: f true ≡ f false → ⊥
observation f (mkEquiv g f∘g g∘f) p =
let
-- Given p : f true ≡ f false
-- Proof strategy is to call g on it to get (g ∘ f) true ≡ (g ∘ f) false
-- Then, use our equivalence properties to reduce it to true ≡ false
-- Then, apply the lemma true≢false we just proved
step1 = ap g p
step2 = sym (g∘f true) ∙ step1 ∙ (g∘f false)
step3 = true≢false step2
in step3
```
For convenience, let's rewrite this so that it takes in an arbitrary $b$ and
returns the version of the inequality we want:
```
observation' : (f : 𝟚𝟚) → isEquiv f → (b : 𝟚) → f b ≢ f (bool-neg b)
-- ^^^^^^^^^^^^^^^^^^^^
observation' f eqv true p = observation f eqv p
observation' f eqv false p = observation f eqv (sym p)
```
Then, solving the `true` case is simply a matter of using function
extensionality (functions are equal if they are point-wise equal) to show that
just the function part of the equivalences are identical, and then using this
property to prove that the equivalences must be identical as well.
```
goal3 eqv true p = equiv-isProp bool-eqv eqv functions-equal
where
e = fst eqv
pointwise-equal : (x : 𝟚) → e x ≡ x
pointwise-equal true = p
pointwise-equal false with ef ← e false in eq = helper ef eq
where
helper : (ef : 𝟚) → e false ≡ ef → ef ≡ false
helper true eq = rec-⊥ (observation' e (snd eqv) false (eq ∙ sym p))
helper false eq = refl
-- NOTE: fst bool-eqv = id, definitionally
functions-equal : id ≡ e
functions-equal = sym (funExt pointwise-equal)
```
The `false` case is proved similarly.
```
goal3 eqv false p = equiv-isProp bool-eqv2 eqv functions-equal
where
e = fst eqv
pointwise-equal : (x : 𝟚) → e x ≡ bool-neg x
pointwise-equal true = p
pointwise-equal false with ef ← e false in eq = helper ef eq
where
helper : (ef : 𝟚) → e false ≡ ef → ef ≡ true
helper true eq = refl
helper false eq = rec-⊥ (observation' e (snd eqv) true (p ∙ sym eq))
functions-equal : bool-neg ≡ e
functions-equal = sym (funExt pointwise-equal)
```
Putting this all together, we get the property we want to prove:
```
open f∘g-case
-- main-theorem : (𝟚𝟚) ≃ 𝟚
main-theorem = g , mkEquiv f g∘f f∘g
```
Now that Agda's all happy, our work here is done!
Going through all this taught me a lot about how the basics of equivalences and
how to express a lot of different ideas into the type system. Thanks for
reading!