PRFA.live_coding7
MPRI 2-7-2 / 2024 / Live coding session 7
Leibniz equality
There is more than one potential definition of equality.
One classical definition of equality on paper is that of Leibniz, saying that
two objects are equal when they have the exactly same properties.
We could say u = v is the same as P u ↔ P v for all predicates P☐.
In fact, is is enough to define u = v as P u → P v for all P because
one can recover the other direction as we shall see:
Goal ∀ {A : Type} (u v : A),
(∀ P : A → Prop, P u ↔ P v) ↔ (∀ P, P u → P v).
Proof.
split.
- firstorder.
- intros. split. eapply H.
apply (H (fun v ⇒ P v → P u)).
auto.
Qed.
Module Leibniz.
Let us now play with this notion of equality. We define it as follows.
We pick a nice notation for it.
We now define reflexivity.
To prove a == a is the same as proving ∀ P, P a → P a.
Seen that way, it should be easy to see what one is supposed to do.
We assume P and some h : P a and we have to prove P a.
That we do using h.
In a sense we are just defining a particular case of the identity function.
Symmetry.
The proof of symmetry is the same as showing that the definition of equality
can be given with both ↔ and →.
Transitivity.
Transitivity is composition:
Lemma trans {A} (u v w : A) (e : u == v) (e' : v == w) : u == w.
Proof.
intro P.
apply e'. apply e.
Defined.
Print trans.
Congruence.
We saw the tactic f_equal in the first lecture. It allows proving
f x = f y from x = y, i.e. lets us apply a function to both sides of an
equality. We have P u → P v for all P, so in particular, by taking
P x := Q (f x) we have Q (f u) → Q (f v) for all Q.
This is how we get f u == f v from u == v.
Lemma f_equal {A B} (f : A → B) {u v} (e : u == v) : f u == f v.
Proof.
intro P.
apply e. apply refl.
Defined.
We can still prove equalities like we did before with this definition.
The main difference is that instead of tactics like f_equal, we have
to apply the lemmas manually.
Lemma n_plus_0 : ∀ n, n + 0 == n.
Proof.
intro n. induction n as [ | n ih ].
- apply refl.
- simpl. apply f_equal. assumption.
Qed.
We can also discriminate between 0 and S n this way.
From S n == 0 we just have to instantiate it with the right P.
We're going to get P (S n) → P 0 so we want P 0 to be False and
P (S n) to be provable, so we pick True.
Lemma nat_discr : ∀ n, S n == 0 → False.
Proof.
intros n e.
specialize (e (fun x ⇒ match x with 0 ⇒ False | _ ⇒ True end)).
simpl in e. apply e. constructor.
Qed.
Coq's standard library does not use Leibniz equality, but an equivalent
definition.
Namely, Coq uses an inductive definition stating that equality is the
smallest reflexive relation. In the end, this is equivalent to Leibniz
equality, but has some advantages as we will see.
Module EqDef. (* Alternatively: Print eq *)
Inductive eq {A} (u : A) : A → Prop :=
| eq_refl : eq u u.
We can see that u is a (uniform) parameter of the definition, whereas
v is an index instantiated to exactly and only u in the (only)
constructor.
As we know, this leads to the following induction principle -- see how
close it is to Leibniz equality?
Check eq_ind.
Lemma eq_to_Leibniz {A} (u v : A) : eq u v → u == v.
Proof.
intros e.
induction e.
apply refl.
Qed.
Lemma Leibniz_to_eq {A} (u v : A) : u == v → eq u v.
Proof.
intros e.
apply e.
apply eq_refl.
Qed.
End EqDef.
End Leibniz.
We go back to using eq with Notation = from the standard library.
We first prove symmetry and transitivity using eq_ind, meaning they have the
essentially same proofs as for Leibniz equality. We then go into details why
eq is defined inductively.
Definition eq_sym {A} {u v : A} (e : u = v) : v = u :=
eq_ind _ (fun x ⇒ x = u) eq_refl _ e.
Definition eq_trans {A} {u v w : A} (e : u = v) (e' : v = w) : u = w :=
eq_ind _ (fun x ⇒ u = x) e _ e'.
Since eq is an inductive type we can also use pattern matching to reason
about it. When matching on an equality e : u = v, the only constructor is
eq_refl forcing v to be exactly u in the eq_refl branch.
This is why, for symmetry, we don't need to prove v = u but actually
u = u and so eq_refl is a valid proof.
For transitivity, we match on e, so v is "unified" with u, meaning we
only need to prove u = w → u = w which is easy.
Note that to do it, we abstract over e' inside and apply it again outside:
This is because the change of v to u only applies to everything *in* the
branch.
In other words, we first write a proof of v = w → u = w using
equality e : u = v, and then we apply it to e' : v = w.
Definition eq_trans' {A} {u v w : A} (e : u = v) (e' : v = w) : u = w :=
match e with
| eq_refl ⇒ fun e' ⇒ e'
end e'.
Fail Definition eq_trans' {A} {u v w : A} (e : u = v) (e' : v = w) : u = w :=
match e with
| eq_refl ⇒ e'
end.
As for all other inductive types, we can also use the destruct tactic on
equalities. It does the same as pattern matching. If you destruct
e : u = v it will unify v with u in the goal, so that f u = f v
becomes f u = f u which is provable by reflexivity.
Lemma f_equal' :
∀ A B (f : A → B) u v, u = v → f u = f v.
Proof.
intros A B f u v e.
destruct e. reflexivity.
Qed.
Definition f_equal'' A B (f : A → B) u v (e : u = v) : f u = f v :=
match e with
| eq_refl ⇒ eq_refl
end.
∀ A B (f : A → B) u v, u = v → f u = f v.
Proof.
intros A B f u v e.
destruct e. reflexivity.
Qed.
Definition f_equal'' A B (f : A → B) u v (e : u = v) : f u = f v :=
match e with
| eq_refl ⇒ eq_refl
end.
So why is eq defined inductively and not as Leibniz equality? The reason
is that Leibniz equality only quantifies over predicates P : X → Prop,
because otherwise it would not live in Prop. However, this means that
Leibniz equality cannot (directly) be used to do rewrites in *types*:
Goal ∀ {A} (u v : A),
Leibniz.leib_eq u v → ∀ P : A → Type, P u → P v.
Proof.
intros A u v e P.
Fail specialize (e P).
Abort.
If we re-examine the definition of equality, we see that it is a
sub-singleton:
- It has one constructor eq_refl.
- This constructor has two parameters A and u and *no* other arguments, meaning in particular all other arguments are proofs.
Goal ∀ {A} (u v : A), u = v → ∀ P : A → Type, P u → P v.
Proof.
intros A u v e P.
rewrite e. auto. Show Proof.
Qed.
Of course, since Leibniz equality is equivalent to eq it could still be
used as definition, but every rewrite in types would require using the
equivalence. Thus, we use eq directly, because it allows both pattern
matching and Leibniz-like reasoning through eq_ind.
The property we just proved is called transport. We prove it again, this
time with a proof term and using eq_rect:
Goal ∀ {A} (u v : A), u = v → ∀ P : A → Type, P u → P v.
Proof.
refine (fun A u v e P ⇒ match e with
| eq_refl ⇒ fun a ⇒ a
end).
Qed.
Definition transport {A} (u v : A) : u = v → ∀ P : A → Type, P u → P v :=
fun e P ⇒ eq_rect u (fun v : A ⇒ P u → P v) (fun a ⇒ a) v e.
We now turn to working with dependent functions.
We prove f_equal for dependent functions:
This definition doesn't work because f u : B u and f v : B v and
B u and B v are not a priori the same type.
Instead, we need to transport f u from B u to B v. And we know how to
do that: we simply use eq_rect on e : u = v to prove B u → B v.
Lemma dep_f_equal :
∀ A B (f : ∀ (x : A), B x) u v (e : u = v),
eq_rect _ B (f u) _ e = f v.
Proof.
intros A B f u v e.
destruct e.
simpl.
Notice how eq_rect computes!
reflexivity.
Qed.
Qed.
Equality can be useful when performing induction.
Inductive even : nat → Prop :=
| even0 : even 0
| evenS : ∀ n, even n → even (S (S n)).
Lemma evenSS_inv :
∀ n,
even (S (S n)) →
even n.
Proof.
intros n h.
destruct h.
With this destruct, Coq will simply forget the index (S (S n)) of
even and only consider the cases 0 and S (S n), even though we know
S (S n) can't be 0.
The remember tactic lets us replace S (S n) by some variable as index,
together with equality e : m = S (S n). Now destruct or induction will
work because it can unify m with 0 or S (S k) easily as it is a
variable. We still get the case m = 0, but now we have equality
0 = S (S n) that we can discriminate to close the goal.
Restart.
intros n h.
remember (S (S n)) as m eqn: e.
destruct h.
- discriminate.
- inversion e. subst. assumption.
Qed.
intros n h.
remember (S (S n)) as m eqn: e.
destruct h.
- discriminate.
- inversion e. subst. assumption.
Qed.
Let's do it with a proof term directly.
Here we use a more advanced pattern matching notation that lets us give a name
to the index of the thing we match on: we write h in even m to give a name
m to the index. We write a return clause that depends on this m that says
m = S (S n) → even n.
So since h : even (S (S n)), the type of the whole match expression is going
to be m = S (S n) → even n where m is replaced by S (S n), in other
words by S (S n) = S (S n) → even n. This is why we apply the whole match
to eq_refl : S (S n) = S (S n) and it then has type even n.
Now we get two branches, one in which m is replaced by 0 and one in which
it is replaced by S (S m). In the 0 branch, we thus have to show
0 = S (S n) → even n. We use eq_ind to go from P 0 to P (S (S n))
such that P 0 is True and P (S (S n)) is even n.
In the S (S m) branch, we have S (S m) = S (S n) so we first prove that
m = n by applying pred on both sides twice and use that equality to go
from even m to even n.
Definition evenSS_inv_alt n (h : even (S (S n))) : even n :=
match h in even m return m = S (S n) → even n
with
| even0 ⇒
fun (e : 0 = S (S n)) ⇒
eq_ind 0 (fun x ⇒ match x with 0 ⇒ True | _ ⇒ even n end) I _ e
| evenS m h' ⇒
fun (e : S (S m) = S (S n)) ⇒
eq_ind _ even h' _ (f_equal (fun x ⇒ pred (pred x)) e)
end eq_refl.
Function extensionality
We can write the following map function such that map f l applies f to
every element of list l.
In other words it turns [ a1 ; a2 ; a3 ; a4 ] into
[ f a1 ; f a2 ; f a3 ; f a4 ].
We can thus show that map (fun x ⇒ x) l = l.
However, we cannot show map (fun (x : A) ⇒ x) = (fun l ⇒ l).
For this we need an extra principle that is often assumed on paper:
function extensionality (FunExt).
It says that two functions are equal if and only iff they yield equal results
on all inputs.
It already appeared in some exercises, but a reminder won't hurt.
Fixpoint map {A B} (f : A → B) (l : list A) : list B :=
match l with
| [] ⇒ []
| x :: l' ⇒ f x :: map f l'
end.
Lemma map_id :
∀ A (l : list A),
map (fun x ⇒ x) l = l.
Proof.
intros A l.
induction l.
- reflexivity.
- simpl. f_equal. assumption.
Qed.
Lemma map_id' :
∀ A,
map (fun (x : A) ⇒ x) = (fun l ⇒ l).
Proof.
intro A.
(* We're actually stuck here! *)
Abort.
Definition FunExt :=
∀ A B (f g : A → B), (∀ x, f x = g x) → f = g.
Axiom funext : FunExt.
Lemma map_id' :
∀ A,
map (fun (x : A) ⇒ x) = (fun l ⇒ l).
Proof.
intro A.
apply funext. intro l.
apply map_id.
Qed.
Discriminating types.
We've seen before how to show true ≠ false or 0 ≠ S n.
We can also distinguish types. We cannot do it the same way as before by
matching on them because we cannot match on types.
Instead we have to exploit properties that hold for one and not the other.
For instance, we can show that unit and bool are different by first
showing that all elements of unit are equal. Then if unit = bool, then all
elements of bool must be equal. But we already know two different elements
of type bool: true and false.
Definition all_equal A := ∀ (x y : A), x = y.
Lemma unit_neq_bool :
unit ≠ bool.
Proof.
intro e.
assert (h : all_equal unit).
{ intros [] []. reflexivity. }
rewrite e in h.
specialize (h true false). discriminate.
Qed.
Another independent principle: proposition extensionality.
It states that two propositions are equal if and only iff they are logically
equivalent, ie that they imply one another.
Using this principle, we can show that propositions with a proof are equal
to the True proposition.
Definition PropExt := ∀ (P Q : Prop), (P ↔ Q) → P = Q.
Axiom propext : PropExt.
Lemma inhabited_is_True :
∀ (P : Prop), P → P = True.
Proof.
intros P h.
apply propext. split.
- intro. constructor.
- intro. assumption.
Qed.
Proof irrelevance.
Proof irrelevance is another property that doesn't hold by default but that
is safe to assume (like PropExt or FunExt).
It says that any two proofs of the same proposition are equal.
We can show that PropExt implies proof irrelevance.
Definition ProofIrrelevance :=
∀ (P : Prop) (p q : P), p = q.
Lemma proof_irrelevance :
(* using PropExt axiom *)
ProofIrrelevance.
Proof.
intros P H1 H2.
pose proof (inhabited_is_True P H1).
subst. destruct H1, H2.
reflexivity.
Qed.
This handy command let us see which axioms were used in the proof.
Uniqueness of identity proofs (UIP).
Another principle that is similar to proof irrelevance is the fact that all
proofs of the same equality are equal.
Definition UIP :=
∀ A (u v : A) (p q : u = v), p = q.
Definition PI_UIP : ProofIrrelevance → UIP.
Proof.
intros pi A u v p q.
apply pi.
Qed.
∀ A (u v : A) (p q : u = v), p = q.
Definition PI_UIP : ProofIrrelevance → UIP.
Proof.
intros pi A u v p q.
apply pi.
Qed.
Boolean equality again.
We recall boolean equality for natural numbers. Then we show that we can
decide wether two natural numbers are equal of not. This is expressed by
the type { n = m } + { n ≠ m } which either contains a proof of n = m
or a proof of the opposite: n ≠ m.
This can be read as a function that computes either a proof that the two
numbers are equal, or disproves it. In other words, it is a decision
procedure for natural number equality.
Fixpoint eq_nat (n m : nat) : bool :=
match n, m with
| 0, 0 ⇒ true
| S n, S m ⇒ eq_nat n m
| _, _ ⇒ false
end.
Lemma eq_nat_eq :
∀ n m, eq_nat n m = true → n = m.
Admitted.
Lemma eq_eq_nat :
∀ n m, n = m → eq_nat n m = true.
Admitted.
Lemma eq_dec :
∀ (n m : nat), { n = m } + { n ≠ m }.
Proof.
intros n m.
destruct (eq_nat n m).
- left. (* We don't know eq_nat n m = true here! *)
Restart.
intros n m.
The eqn below also remembers that eq_nat n m = true or false depending
on the branch we're in.