PRFA.live_coding5
MPRI 2-7-2 / 2024 / Live coding session 5
- 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.
Sorts
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.
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.
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.
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".
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.
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
| ptrue ⇒ pfalse
| pfalse ⇒ ptrue
end.
Lemma pneg_invol :
∀ b, pneg (pneg b) = b.
Proof.
intros []. all: reflexivity.
Qed.
We can also map the usual booleans into them.
But the converse we can't.
Coq complains that proofs can only be eliminated to build other proofs.
The same happens in proof mode of course.
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
- 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 a ⇒ bt_leaf (f a)
| bt_node a l r ⇒ bt_node (f a) (bt_map f l) (bt_map f r)
end.
Lemma bt_map_id :
∀ A (t : bintree A),
bt_map (fun x ⇒ x) 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 a ⇒ node a []
| bt_node a l r ⇒ node 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 l ⇒ node (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 x ⇒ x) 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:
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 x ⇒ x) 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.
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.
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.
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.
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 x ⇒ x) 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 x ⇒ x) 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
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
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.
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 n ⇒ path (S n)).
firstorder.
Qed.
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 n ⇒ path (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 k ⇒ f 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 n ⇒ f 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.