PRFA.live_coding5

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

The goal of this lecture is
  • understand more about sorts in Coq, i.e. about Type and Prop. This is helpful to decipher certain error messages that occasionally occur in practical developments.
  • understand how induction principles are implemented and use this knowledge to implement induction principles for inductive types where Coq's automatic generation is too weak.

From Coq Require Import Arith Lia List.
Import ListNotations.

Sorts

We have already seen Type and Prop. They We are also called "universes" or "sorts". You have seen that seemingly Type : Type

Check (Type : Type).

but also that Type is actually a hierarchy of universes Type@{i} in order to avoid inconsistencies with Type : Type, which becomes visible once universe printing is enable. Note that in VSCoq you have to use the command palette (Ctrl/Cmd + Shift + P) and then "Coq: Display Universe levels".

Set Printing Universes.

Check (Type : Type).

Below we will see more about the typing rules of Type, in particular a reminder how the typing rule of forall works.

Section Universes.

  Universes i j.
  Constraint i < j.
  Check Type@{i} : Type@{j}.
  Fail Check Type@{j} : Type@{i}.

  Universe k.

  Variable (A : Type@{i}) (B : Type@{k}).

Universe of foralls.
As you can see below, the type of a forall is the maximum of the types of the domain and codomain. See what it means for a forall containing Type.

  Check (a : A), B.

  Check (X : Type@{i}), Type@{k}.

Cumulativity
Another property of Coq (and not present in Lean or Agda) is called cumulativity. It is a form of subtyping that says that Type@{i} is a subtype of Type@{j} when i ≤ j. See how A : Type@{i} also lives in Type@{j} but not the converse.

  Check (A : Type@{j}).

  Variable (C : Type@{j}).

  Fail Check (C : Type@{i}).

Prop lives in Type@{i} for all i.

  Check (Prop : Type@{i}).

Here you see Coq mentions a constraint that Set < i. There is indeed a third kind of universe we did not mention but you may have seen already. It's called Set. You can think of it as the lowest Type universe: Type@{0}, but in Coq it's actually Type@{Set} because the universe levels (i, j, k etc.) are not natural numbers but abstract variables with constraints like the one we added above.
Set is the smallest Type and it is the only one which does not contain Prop. We do have cumulativity going from Prop to Set and Type however.
An overview:
Prop : Type@{Set+1} Set : Type@{Set+1} Prop <= Type@{Set+1} Set <= Type@{Set+1} Type@{i} : Type@{i+1} Type@{i} <= Type@{i+1}

  Fail Check (Prop : Set).
  Fail Check (Prop : Type@{Set}).

  Variable (P : Prop).

  Check (P : Type@{i}).
  Check (P : Set).

Impredicativity.
Prop has other particularities. When considering a forall that ends in a proposition, then the whole forall stays a proposition. This means that things like forall n, n = n or forall (P : Prop), P are propositions. This is impredicative because things like forall (P : Prop), P are actually quantifying over themselves.
This is another big difference with Agda which is predicative instead. To remedy this they have to also add universe levels to Prop like Prop@{i}.
In Coq, the universe of a forall ending in a proposition is always a proposition, regardless of the size of what we quantify over. This comes from the intuition (and models) that Prop is proof-irrelevant: all propositions are morally either empty (False) or contain only one element (True) so Prop can be seen (modelled) as a set with two elements so it is always small.

  Fail Check (Prop : Prop).

  Check (P : Prop), P.
  Check (n : nat), n = n.

  Variable (Q : A Prop).

  Check (x : A), Q x.

  Check (X : Type@{j}), P.

  Unset Printing Universes.
Again on VSCoq, use the palette to deactivate it. The same command works to disable universe printing.

End Universes.

Elimination restrictions
Universes are also important when defining inductive types. When should you put your type in Prop or in Type? We have seen several examples: things like disjunction and conjunction are placed in Prop, but bool : Type for instance.
The central reasons are the Prop is an impredicative sort (more on that below), but also that Prop can be axiomatically assumed to be "proof irrelevant".

Definition PI := P : Prop, h1 h2 : P, h1 = h2.

In other words, this means that proofs cannot impqact computation in a meaningful way. That is, we cannot analyse a proof to build a a function that is supposed to compute a boolean.

Inductive prop_bool : Prop := ptrue | pfalse.

As long as we remain in Prop, we can do pretty much the same as before.

Definition pneg (b : prop_bool) : prop_bool :=
  match b with
  | ptruepfalse
  | pfalseptrue
  end.

Lemma pneg_invol :
   b, pneg (pneg b) = b.
Proof.
  intros []. all: reflexivity.
Qed.

We can also map the usual booleans into them.

Definition to_prop_bool (b : bool) : prop_bool :=
  if b then ptrue else pfalse.

But the converse we can't.

Fail Definition to_bool (b : prop_bool) : bool :=
  match b with
  | ptruetrue
  | pfalsefalse
  end.

Coq complains that proofs can only be eliminated to build other proofs. The same happens in proof mode of course.

Lemma to_bool : prop_bool bool.
Proof.
  intro b. Fail destruct b.
Abort.

We can instead prove something weaker like this:

Lemma to_bool :
   (p : prop_bool),
     (b : bool), to_prop_bool b = p.
Proof.
  intros [].
  - true. reflexivity.
  - unshelve eexists ; econstructor.
Qed.

We can however not prove that there is a **function** inverting to_prop_bool, because this would be inconsistent with PI:

Lemma to_bool_function_not_PI (f : prop_bool bool) :
  ( b, f (to_prop_bool b) = b) ¬ PI.
Proof.
  intros H pi.
  specialize (pi prop_bool ptrue pfalse).
  specialize (H true) as e1.
  specialize (H false) as e2.
  simpl in ×.
  rewrite pi in e1.
  rewrite e2 in e1.
  discriminate.
Qed.

Singleton elimination
However, there are crucial exceptions to the rule: If a type has at most one constructor and all the non-parameter arguments of the constructor are proofs, then such eliminations are allowed.
This property is called (sub)singleton elimination because it essentially says that you can eliminate a proof, as long as there can be at most one such proof. So a term that depends on it, doesn't really depend on it in the end, but merely on the fact that there exists a proof.
Note that they don't need to be in (sub)singletons but only in Prop. See the example below.

Goal False nat.
Proof.
  intro bot. destruct bot.
Qed.

Definition exfalso' {A : Type} (bot : False) : A :=
  match bot with end.

It also works with True, but it's not so interesting. Another more interesting case is equality. This means, you can use tactics such as rewrite on any type.

Goal n m (P : nat Type), P (n + m) P (m + n).
Proof.
  intros n m P x.
  rewrite PeanoNat.Nat.add_comm. assumption.
Qed.

The above we can do in general:

Goal {A} (P : A Type) {u v} (e : u = v) (t : P u), P v.
Proof.
  intros A P u v e. now rewrite e.
Qed.

Disjunction for instance does not fall under the exception:

Goal P Q, P Q bool.
Proof.
  intros P Q h.
  Fail destruct h.
Abort.

Inductive Box (P : Prop) : Prop :=
| box (p : P).

Goal P Q, Box (P Q) bool.
Proof.
  intros P Q h.
  destruct h.
  Fail destruct h.
Abort.

Induction principles

Trees
We can easily define binary trees (with labels in A) as follows. As for every inductive type, Coq generates induction principles corresponding to the various sorts:
  • bintree_ind for Prop
  • bintree_rect for Type
  • bintree_rec for Set (but you can ignore it and use bintree_rect)

Inductive bintree A :=
| bt_leaf (a : A)
| bt_node (a : A) (l r : bintree A).

Arguments bt_leaf {A}.
Arguments bt_node {A}.

Check bintree_rect.

Fixpoint bt_map {A B} (f : A B) (t : bintree A) : bintree B :=
  match t with
  | bt_leaf abt_leaf (f a)
  | bt_node a l rbt_node (f a) (bt_map f l) (bt_map f r)
  end.

Lemma bt_map_id :
   A (t : bintree A),
    bt_map (fun xx) t = t.
Proof.
  intros A t.
  induction t as [a | a l ihl r ihr].
  - reflexivity.
  - cbn. rewrite ihl, ihr. reflexivity.
Qed.

General trees
Now say we don't want to limit ourselves to binary trees. The obvious way to define trees is to have nodes take a list of subtrees. Now as we're going to see, the induction principle that Coq generates is insufficient.

Inductive tree A :=
| node (a : A) (children : list (tree A)).

Arguments node {A}.

Fixpoint bt_to_tree {A} (t : bintree A) : tree A :=
  match t with
  | bt_leaf anode a []
  | bt_node a l rnode a [ bt_to_tree l ; bt_to_tree r ]
  end.

Fixpoint tree_map {A B} (f : A B) (t : tree A) : tree B :=
  match t with
  | node a lnode (f a) (map (tree_map f) l)
  end.

Lemma to_tree_map :
   A B (f : A B) t,
    tree_map f (bt_to_tree t) = bt_to_tree (bt_map f t).
Proof.
  intros A B f t.
  induction t as [a | a l ihl r ihr].
  - reflexivity.
  - cbn. rewrite ihl, ihr. reflexivity.
Qed.

Lemma tree_map_id :
   A (t : tree A),
    tree_map (fun xx) t = t.
Proof.
  intros A t.
  induction t.
  cbn. f_equal.
We have no induction hypopthesis at all!!
Abort.

Indeed, see what Coq generated for us:
Check tree_rect.
It essentially corresponds to a case analysis, but no induction hypotesis is avaiable. This is because we are using what we call *nested* inductive types. Coq does not know what to do with assumptions of type list so it just does nothing.
Of course, this is stupid and we probably still have to suffer it because of backwards compatibility. Let's see how we can do better.
First, notice that Coq still supports this kind of induction by using fixpoints. We can actually use them interactively.

Fixpoint tree_map_id A (t : tree A) {struct t} : tree_map (fun xx) t = t.
Proof.
  destruct t.
  cbn. f_equal.
  erewrite map_ext.
  - apply map_id.
  - cbn. intros x. apply tree_map_id.
Fail Qed.
Qed fails here because Coq cannot see our fixpoint terminates. Indeed, we used a recursive call on something completely unrelated. The reason we only know now is that Coq only checks for termination at Qed time, and not before. There is way to know beforehand though: the Guarded command checks that the guard condition (which verifies termination) holds up to the point where it's called.
Restart.
  destruct t.
  cbn. f_equal.
  Guarded.
  erewrite map_ext_in.
  - apply map_id.
  - Guarded. cbn. intros x h. Guarded. apply tree_map_id.
Fail Qed.
This time it should actually work because morally we only use recursive calls on elements *in* the list, but Coq doesn't know it.
Restart.
  destruct t.
  cbn. f_equal.
We in fact need to be able to recurse on the list, so we the same way we nested inductive types, we need to nest fixpoints. For this we can use the fix tactic which introduces a fixpoint. We need to revert the argument on which we want to recurse and then fix aux 1 will introduce a new hypothesis named aux corresponding to potential recursive calls.
  revert children. fix aux 1. intro l.
  destruct l as [| x l].
  - reflexivity.
  - cbn. f_equal.
    + apply tree_map_id.
    + apply aux.
Qed.

This was rather painful because we have manually ensure that Coq recognises the proof to be structurally recursive. Ideally we would like to be able to do induction like always. In order to do so, we are going to use fixpoints to prove our own induction principle for trees.
We'll start by copying the type of tree_ind.

Check tree_ind.

We then add an induction hypothesis for all elements of the children list

Lemma good_tree_ind' :
   A (P : tree A Prop),
    ( a l, ( x, In x l P x) P (node a l))
     t, P t.
Proof.
  intros A P h.
  fix ind 1.
  intros [ a l ].
  apply h.
  revert l. fix aux 1. intro l.
  destruct l as [| x l].
  - intros y Hy. destruct Hy.
  - intros y Hy.
    destruct Hy as [ <- | Hy].
    + apply ind.
    + apply (aux l). assumption.
Qed.

Instead of using In, we can also use the Forall predicate

Print Forall.

Lemma good_tree_ind :
   A (P : tree A Prop),
    ( a l, Forall P l P (node a l))
     t, P t.
Proof.
  intros A P h.
  fix ind 1.
  intros [ a l ].
  apply h.
  revert l. fix aux 1. intro l.
  destruct l as [| x l].
  - constructor.
  - constructor.
    + apply ind.
    + apply aux.
Qed.

We can now use this induction principle by hand to prove tree_map_id again:

Lemma tree_map_id_again :
   A (t : tree A),
    tree_map (fun xx) t = t.
Proof.
  intros A t.
  pattern t.
  apply good_tree_ind.
  clear t. intros a l h. cbn. f_equal.
  induction h as [| tr l e h ih].
  - reflexivity.
  - cbn. f_equal.
    + apply e.
    + apply ih.
Qed.

In fact, we can still use the induction tactic to do it. This is done with the special "using" keyword as below.

Lemma tree_map_id_again' :
   A (t : tree A),
    tree_map (fun xx) t = t.
Proof.
  intros A t.
  induction t as [a l ihl] using good_tree_ind.
  cbn. f_equal. induction ihl.
  - reflexivity.
  - cbn. f_equal. all: assumption.
Qed.

Size recursion

We have already seen size induction earlier. We can use the same idea to prove a size recursion principle:

Lemma size_recursion (X : Type) (sigma : X nat) (p : X Type) :
  ( x, ( y, sigma y < sigma x p y) p x)
   x, p x.
Proof.
  intros D x. apply D.
  cut ( n y, sigma y < n p y).
  now eauto.
  clear x. intros n.
  induction n; intros y E.
  - exfalso. lia.
  - apply D. intros x F. apply IHn. lia.
Defined.

Well-founded induction

The last topic how to do induction / recursion along *any* well-founded order. We define well-foundedness of an order on a point as the so-called Accessibility predicate:

Section Well_founded.

  Variable A : Type.
  Variable R : A A Prop.

  Inductive Acc (x: A) : Prop :=
    (* A point x is accessible if all its predecessors in the relation are *)
    Acc_intro : ( y:A, R y x Acc y) Acc x.

  Fact Acc_iff x :
    Acc x y, R y x Acc y.
  Proof.
    split.
    - intros [h]. exact h.
    - now constructor.
  Qed.

End Well_founded.

Check Acc_rect.

We can prove that accessible points are not contained in loops:
Fact exercise_loop (X: Type) (R: X X Prop) x :
  Acc _ R x ¬ y, R x y R y x.
Proof.
  intros H.
  induction H as [x H IH].
  firstorder.
Qed.

And that accessible points are not the start of an infinite descending path
Fact exercise_chain (X: Type) (R: X X Prop) x :
  Acc _ R x ¬ path : nat X, path 0 = x n, R (path (S n)) (path n).
Proof.
  intros H.
  induction H as [x H IH].
  intros (path & H1 & H2). subst.
  eapply IH. eapply H2.
   (fun npath (S n)).
  firstorder.
Qed.

We give a proof that one can use recursion on proofs to write a function that "extracts" the witness from proofs of n, f n = true.
This proof can be given using different ways. We show the easiest approach here, and leave a variant using Acc as exercise.

Require Import Lia.

Definition least p n k := n k p k i, n i p i k i.

Section WO.

  Variable f: nat bool.

  (* Guardedness predicate *)
  Inductive G (n: nat) : Prop :=
  | GI : (f n = false G (S n)) G n.

  Lemma G_sig n :
    G n { k | least (fun kf k = true) n k }.
  Proof.
    induction 1 as [n _ IH].
    destruct (f n) eqn:H.
    - n. repeat split; eauto.
    - destruct (IH eq_refl) as (k & Hle & Hk & Hleast).
       k. repeat split.
      + lia.
      + exact Hk.
      + intros i Hi. inversion Hi.
        × congruence.
        × eapply Hleast; eauto. lia.
  Defined.

  Lemma G_zero n :
    G n G 0.
  Proof.
    induction n as [|n IH].
    - intros H. exact H.
    - intros H. apply IH. constructor. intros _. exact H.
  Defined.

  Theorem linear_search :
    ( n, f n = true) { n | least (fun nf n = true) 0 n }.
  Proof.
    intros H. apply (G_sig 0).
    destruct H as [n H].
    apply (G_zero n).
    constructor. rewrite H. discriminate.
  Defined.

End WO.

About linear_search.