PRFA.live_coding7

MPRI 2-7-2 / 2024 / Live coding session 7

In this session we are looking at the definition of equality (u = v) in Coq.
You have already used equality with rewrite, subst, reflexivity, etc., but we have until now avoided spelling out the definition.

From Coq Require Import List.
Import ListNotations.

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 vP v P u)).
    auto.
Qed.

Module Leibniz.

Let us now play with this notion of equality. We define it as follows.

  Definition leib_eq {A} (u v : A) : Prop :=
     P : A Prop, P u P v.

We pick a nice notation for it.

  Notation "u == v" := (leib_eq u v) (at level 80).

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.

  Definition refl {A} (a : A) : a == a :=
    fun P (h : P a) ⇒ h.

Symmetry.
The proof of symmetry is the same as showing that the definition of equality can be given with both and .

  Definition sym {A} (u v : A) (e : u == v) : v == u :=
    fun Pe (fun xP x P u) (refl u P).

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 xmatch 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 xx = u) eq_refl _ e.

Definition eq_trans {A} {u v w : A} (e : u = v) (e' : v = w) : u = w :=
  eq_ind _ (fun xu = 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.
Definition eq_sym' {A} {u v : A} (e : u = v) : v = u :=
  match e with
  | eq_refleq_refl
  end.

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_reflfun 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_refle'
  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_refleq_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 Pmatch e with
          | eq_reflfun aa
          end).
Qed.

Definition transport {A} (u v : A) : u = v P : A Type, P u P v :=
  fun e Peq_rect u (fun v : AP u P v) (fun aa) v e.

We now turn to working with dependent functions.
We prove f_equal for dependent functions:

Fail Lemma dep_f_equal :
   A B (f : (x : A), B x) u v,
    u = v
    f u = f v.

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.

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.

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 xmatch 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 xpred (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 xx) l = l.
Proof.
  intros A l.
  induction l.
  - reflexivity.
  - simpl. f_equal. assumption.
Qed.

Lemma map_id' :
   A,
    map (fun (x : A) ⇒ x) = (fun ll).
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 ll).
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.
Print Assumptions proof_irrelevance.

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.

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 meq_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.
  destruct (eq_nat n m) eqn: e.
  - left. apply eq_nat_eq. assumption.
  - right. intro e'. apply eq_eq_nat in e'.
    rewrite e' in e. discriminate.
Qed.