Skip to content

Commit b1950f8

Browse files
JamieTKirkgallais
authored andcommitted
[week 3] annotated lectures
1 parent e8d7bd2 commit b1950f8

File tree

2 files changed

+331
-0
lines changed

2 files changed

+331
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
-- These are the annotated notes which include more detail about what the code is doing.
2+
3+
open import Relation.Binary.PropositionalEquality using (_≡_; refl; subst; cong)
4+
open import Data.Nat.Base using (ℕ; zero; suc; _+_; _*_)
5+
6+
{-
7+
Last time we looked at subst, aka Leibniz equality, which lets us substitute values that are equal for each other. Now we can try to prove some of our equality properties in terms of subst rather than refl. Our implicit claim is that anything we want to state about equality, we can do just with subst.
8+
-}
9+
10+
11+
-- Let's start with symmetry.
12+
sym : {X : Set} -> {x y : X} -> x ≡ y -> y ≡ x
13+
sym {x = x} x≡y = subst (\ z -> z ≡ x) x≡y refl
14+
{-
15+
Syntax note: \ is the same as λ, it indicates the start of an anonymous function.
16+
17+
If we wanted to prove this with refl, we just pattern match on p and see that our goal is easy. But we want to try and prove this with subst instead. We can start with sym p = { subst } and use C-u C-u C-c C-. to normalise and view subst relative to our goal. Our relevant bit is the last part of it: x ≡ y -> P x ≡ P y. If we can prove x equals y, any property that holds of x, holds of y.
18+
19+
Thus we need two arguments to subst - a predicate to prove of x, plus a proof that x ≡ y. This is the hard part: what predicate do we choose? In essence, this is the key challenge of using subst for proofs.
20+
21+
If we hand over subst ? x≡y, and check subst again, we can see that we now have ?3 x -> ?3 y. In other words, if we can come up with a predicate we can prove for x, then we can also prove it for y. This is a much less scary goal, and crucially, it gives us a clue about what predicate we want. We need something that a) is true for x and b) when is instantiated for y, gives us what we want. Looking at our goal, clearly we want a proof that something is equal to x! For this we will need to quickly bring x into scope on the left hand side, and then we have a very simple predicate.
22+
23+
Once we arrive at "sym {x = x} p = {subst (\ z -> ≡ x) p}", we can normalise and check one more time, and see that we now have "x ≡ x -> y ≡ x". Our last thing to do is to add one more argument, namely that x ≡ x, which we can prove with refl. And we're done!
24+
25+
Thinking broadly, when trying to prove using subst, we want to abstract away everything *except* the goal. When trying to work out what predicate to use, we want to look at the goal and see what we're trying to capture. Here, we want to turn ys into xs, so that's what we want to capture with our lambda.
26+
-}
27+
28+
29+
-- Let's see transitivity next.
30+
trans : {X : Set} -> {x y z : X} -> x ≡ y -> y ≡ z -> x ≡ z
31+
trans {x = x} {y} {z} x≡y y≡z = subst (\ y -> y ≡ z -> x ≡ z) x≡y (\ r -> r) y≡z
32+
{-
33+
Here we have two possible equalities we could try working with, x ≡ y and y ≡ z. For simplicity's sake, we'll start with the first one, {subst ? x≡y}. This gets us to a similar status as before: we now have ?3 x -> ?3 y. Our goal is x ≡ z, and looking at our type signature we can see that means that, since we're using x≡y, our subst goal should now be the rest of our type, namely y ≡ z -> x ≡ z. So we can try copying that right in, {subst (\ y -> y ≡ z -> x ≡ z) p}.
34+
35+
Now we have (x ≡ z -> x ≡ z) -> y ≡ z -> x ≡ z, so we can see that subst will need two more arguments to finish: one that proves (x ≡ z -> x ≡ z), and one that proves y ≡ z. These are both now fairly trivial: we can prove the first with (\ r -> r), and the second using y≡z.
36+
37+
A note - since y≡z is simply introduced on the left and then given on the right, we can actually omit writing it altogether if we want to. However, it can increase clarity to be explicit about it, so if you prefer to have it written out that's fine.
38+
-}
39+
40+
41+
-- We now want to generalise this slightly, by proving that propositional eq and Leibniz eq are equivalent.
42+
infix 4 _≡ᴸ_
43+
44+
_≡ᴸ_ : {X : Set} (x y : X) -> Set₁
45+
x ≡ᴸ y = (P : _ -> Set) -> P x -> P y
46+
-- NB subscript and superscript can be typed with \_ and \^ respectively.
47+
48+
-- Let's start by turning propositional into Leibniz:
49+
toLeibniz : {X : Set} {x y : X} -> x ≡ y -> x ≡ᴸ y
50+
toLeibniz x≡y P px = subst P x≡y px
51+
{-
52+
Normalising the goal, we can see we are trying to prove (P : X -> Set) -> P x -> P y. (Again, this means we're proving anything that is true is about x, is true about y.) This looks an awful lot like the statement of subst. Let's start by introducing some predicate P and px : P x (representing the knowledge that that predicate is true for x). Now we need to prove P y. If we place "subst P x≡y" in the goal, we can see that we now have P x -> P y. We have a thing of type P x - it's px! So add that at the end, and we're done.
53+
54+
Leibniz equality essentially says "the equality that does what subst does", so it's fairly obvious that we just need to follow through the steps of subst to prove it.
55+
-}
56+
57+
-- And now the other way:
58+
fromLeibniz : {X : Set} {x y : X} -> x ≡ᴸ y -> x ≡ y
59+
fromLeibniz {x = x} x≡ᴸy = x≡ᴸy (\ y -> x ≡ y) refl
60+
{-
61+
Normalising this, we can see we now *have* (P : X -> Set) -> P x -> P y. This is very useful, as this P will let us prove all sorts of things. Here, we have a problem that's talking in terms of y, so we literally just take a y and ask it to prove x ≡ y (we need to bring x into scope for this). Now we have x ≡ x -> x ≡ y, so adding a simple refl to the end gets us there.
62+
63+
It's important here not to overthink. We could spend forever stressing about "how do I choose P?", but we are allowed to look at our goal. Make P whatever will get us to the goal - here, that taking a y shows x ≡ y!
64+
-}
65+
66+
67+
{-
68+
We now have a slight change of scene, as we start looking at the Curry-Howard principle. This allows us to use Agda's type system to form and then prove statements, by applying deduction steps. The trick is that these deduction steps are just functions we can use. Remember - functions, programs and proofs are really all the same.
69+
70+
Earlier, we struggled to prove (n + 1) ≡ (1 + n), and had to prove it by induction (by taking n apart). We also *failed* to prove 17 ≡ 42, as it's clearly not true.
71+
72+
Now we want to look at finding a way to different between failing to prove and *disproving*, ie proving false. This is the difference between saying "I can't find a proof that 17 ≡ 42 is true" and saying "I can find proof that 17 ≡ 42 is *not* true". We are now proving that a proof *doesn't exist*, not just that we aren't looking at it.
73+
74+
In order to define what it means for something to be false, we need a new type - the empty type. Since we're expressing what it means to be empty, this type should have no constructors.
75+
-}
76+
data : Set where
77+
78+
79+
-- Now we can express that 17 ≡ 42 is nonsense.
80+
_ : 1742 ->
81+
_ = \ ()
82+
{-
83+
The () is what we call the "absurd pattern" - it indicates that there are no valid arguments for this constructor. Essentially it is an impossible pattern - here, it encodes a statement that there is no way to construct 17 ≡ 42. Since there is no way to build the empty type, then a proof of 17 ≡ 42 -> ⊥ means that something has gone wrong if we are able to get a value out of this function. We can think of () as a counterpart to refl: where refl gives trivial equality, () gives trivial emptiness.
84+
-}
85+
86+
-- We can see another, more pedestrian way to do this:
87+
0≠1 : 01
88+
0≠1 = \ p subst P p 7
89+
where
90+
P : Set
91+
P 0 =-- something inhabited
92+
P 1 =-- something empty
93+
P n =-- anything
94+
{-
95+
Rather than just throwing stuff away, we can now prove that this is not true by demonstrating that we can write a function that can tell 0 apart from 1. If a deterministic function returns two different outputs on two given inputs, clearly those inputs are not the same.
96+
97+
Here, our function P just takes a natural number and returns a set. For P 0, we return the natural numbers, but for P 1 we return ⊥. Thus clearly 0 and 1 are not equal.
98+
-}
99+
100+
101+
{-
102+
If we have a proof of the empty type, then we can do whatever we like. We can think of this as arriving at a proof of false in natural logic - clearly it cannot be the case, so we are allowed to assume whatever we like because it is not possible.
103+
104+
In Agda, this is a very useful property as it allows us to implement *explosion* - a way of discarding "dead" branches that are impossible to reach. Essentially, we can use a proof of ⊥ to say that the branch has whatever type we want, because we know the branch will never actually be reached.
105+
-}
106+
explosion :-> {A : Set} -> A
107+
explosion ()
108+
{-
109+
If we bind our proof of ⊥, Agda will instantly be able to recognise when we case split that we are looking for our absurd pattern. Essentially, instead of specifying what our *output* should be, we're explaining why our *input* is impossible: because the input is impossible, Agda doesn't have to bother producing A. We can think of this as a normal function that takes every possible input and produces an output - it just so happens that there are no inputs.
110+
-}
111+
112+
113+
-- Let's prove inequality between something more complex:
114+
broken : _+_ ≡ _*_ ->
115+
broken eq = 0≠1 (sym (cong (\ f -> f 1 0) eq))
116+
{-
117+
Agda can neither disprove it nor find a way to solve it on its own: it's just too complex. However, from our perspective, we just need to find a way to reduce our inputs down into a specific impossible case. So, we choose to prove that 0+1 ≠ 0*1, which is a concrete case that Agda can evaluate. We do this by applying congruence and specifying a function from ℕ -> ℕ -> ℕ, which can be either our + or our *. Agda will then realise that these produce different outputs when fed 1 and 0, and thus we arrive at a proof of ⊥ - it is false.
118+
119+
Broadly, we can think of disproving as finding a specific observation that pushes things apart. Coming up with the case that will demonstrate inequality can take a bit of thinking. Agda can come up with these test for telling different constructors of the same datatype apart, but not much more than that. Here, we've refined a problem of reasoning about some functions to telling that zero and suc n are not the same constructor.
120+
-}
121+
122+
123+
-- We can move on to looking at negation.
124+
Not : Set -> Set
125+
Not X = X ->
126+
{-
127+
The negation of any X is a function from X to the empty type. Since a function is a promise to give back an answer, then clearly we can see here that we cannot return ⊥, so there'd better be no such thing as X. Essentially, "Not X" claims that X is an uninhabited type.
128+
-}
129+
130+
-- We can demonstrate this simply by proving Not ⊥ (not false).
131+
_ : Not ⊥
132+
_ = \ x -> x
133+
-- This is a function from ⊥ -> ⊥, and luckily, we know one - id.
134+
135+
136+
-- Something more interesting: double negation introduction.
137+
dni : {X : Set} -> X -> Not (Not X)
138+
dni x notx = notx x
139+
{-
140+
This says "if we have proof of an X, we should be able to prove that Not X is false". Here, we pull the internal Not X out of the type and make it an argument. Now we can feed our proof of x into the "function" of Not X, and thus produce the empty type. It may help to think about what this typeline is really saying: since Not X is a function, this really says:
141+
dni : {X : Set} -> X -> (X -> ⊥) -> ⊥
142+
which is more obviously provable, as the absurdity of having both X and X -> ⊥ is clear in the type.
143+
-}
144+
145+
146+
-- The trickier one is the counterpart, double negation elimination.
147+
dne : {X : Set} -> Not (Not X) -> X
148+
dne notnotx = {! !}
149+
{-
150+
This says "if the absence of something is unthinkable, then we know how to make it". For booleans (eg when thinking about circuits) this is clearly true. Here, where we don't know what our type is, it's not so obvious. We don't know if X is inhabited or not, so we have two possible strategies and we don't know which one to pursue. If X were inhabited, we'd just give it back, and if X *isn't* inhabited, we have (⊥ -> ⊥) -> ⊥, which is easy. The problem is, we don't know which one we have, so we currently have no hope of implementing this.
151+
-}

0 commit comments

Comments
 (0)