Lab 2: Debrief
- Author
Dario Halilovic, Yawen Guan, Jérémy Thibault, Clément Pit-Claudel
From Stdlib Require Import Nat Lia Arith. Open Scope N_scope.
(There are more tips in Lab 1's debrief — have a look if you missed it!)
bst as an inductive proposition
Our bst fixpoint from Lab 2 could be equivalently rewritten as an inductive proposition:
Inductive tree : Set := | Leaf | Node (l : tree) (n : nat) (r : tree). Inductive bst : tree -> (nat -> Prop) -> Prop := | bst_leaf : forall s (s_empty : forall x, ~s x), bst Leaf s | bst_node : forall s tl d tr (left_bst : bst tl (fun x => s x /\ x < d)) (right_bst : bst tr (fun x => s x /\ x > d)) (d_elem : s d), bst (Node tl d tr) s.
This inductive definition is equivalent to the one given in the lab, but is nicer to handle, as the generated induction principle gives us the shape of the tree "for free". For example:
forall (tr : tree) (P Q : nat -> Prop), bst tr P -> (forall x : nat, P x <-> Q x) -> bst tr Qforall (tr : tree) (P Q : nat -> Prop), bst tr P -> (forall x : nat, P x <-> Q x) -> bst tr Qtr: tree
P, Q: nat -> Prop
Hbst: bst tr P(forall x : nat, P x <-> Q x) -> bst tr Q(* We induct on bst instead of tr! *)tr: tree
P: nat -> Prop
Hbst: bst tr Pforall Q : nat -> Prop, (forall x : nat, P x <-> Q x) -> bst tr Qs: nat -> Prop
s_empty: forall x : nat, ~ s x
Q: nat -> Prop
H: forall x : nat, s x <-> Q xbst Leaf Qs: nat -> Prop
tl: tree
d: nat
tr: tree
Hbst1: bst tl (fun x : nat => s x /\ x < d)
Hbst2: bst tr (fun x : nat => s x /\ x > d)
d_elem: s d
IHHbst1: forall Q : nat -> Prop, (forall x : nat, s x /\ x < d <-> Q x) -> bst tl Q
IHHbst2: forall Q : nat -> Prop, (forall x : nat, s x /\ x > d <-> Q x) -> bst tr Q
Q: nat -> Prop
H: forall x : nat, s x <-> Q xbst (Node tl d tr) Qs: nat -> Prop
s_empty: forall x : nat, ~ s x
Q: nat -> Prop
H: forall x : nat, s x <-> Q xbst Leaf Qfirstorder.s: nat -> Prop
s_empty: forall x : nat, ~ s x
Q: nat -> Prop
H: forall x : nat, s x <-> Q xforall x : nat, ~ Q xconstructor; firstorder. Qed.s: nat -> Prop
tl: tree
d: nat
tr: tree
Hbst1: bst tl (fun x : nat => s x /\ x < d)
Hbst2: bst tr (fun x : nat => s x /\ x > d)
d_elem: s d
IHHbst1: forall Q : nat -> Prop, (forall x : nat, s x /\ x < d <-> Q x) -> bst tl Q
IHHbst2: forall Q : nat -> Prop, (forall x : nat, s x /\ x > d <-> Q x) -> bst tr Q
Q: nat -> Prop
H: forall x : nat, s x <-> Q xbst (Node tl d tr) Q
Using lemmas about compose
Some of you might have noticed that attempting to use rewrite on the ~= relation will not work out of the box. The reason for that is that rewrite does not rewrite under binders:
A, B, C: Typeforall (f g : A -> B) (h : B -> C), f ~= g -> h ∘ f ~= h ∘ gA, B, C: Typeforall (f g : A -> B) (h : B -> C), f ~= g -> h ∘ f ~= h ∘ gA, B, C: Type
f, g: A -> B
h: B -> C
H: f ~= gh ∘ f ~= h ∘ gA, B, C: Type
f, g: A -> B
h: B -> C
H: f ~= g(fun x : A => h (f x)) ~= (fun x : A => h (g x))A, B, C: Type
f, g: A -> B
h: B -> C
H: f ~= g(fun x : A => h (f x)) ~= (fun x : A => h (g x))
Instead, one first has to remove the (hidden) quantifier in ~= before proceeding:
A, B, C: Type
f, g: A -> B
h: B -> C
H: f ~= g
x: Ah (f x) = h (g x)reflexivity. Qed.A, B, C: Type
f, g: A -> B
h: B -> C
H: f ~= g
x: Ah (g x) = h (g x)
Backward vs forward reasoning
There exists two styles in which one can do proofs in Rocq:
The forward style is the traditional mathematical style, where to prove that
P -> Qone sequentially proves thatP -> P1,P1 -> P2, and so on, until finallyQis reached.The backward style is the opposite: to prove
P -> Q, one derives sufficient assumptions in whichQholds, and progressively reduces those assumptions untilPis reached.
Each style has its own trade-offs: the forward style is more natural to mathematicians (i.e. closer to textbook practice), but leads to more verbose proof scripts; on the other hand, the backward style tends to produce shorter (but less readable) proofs, but is more amenable to proof search.
Rocq favors the backward style for its conciseness, and most tactics you'll see work in this style. For example, the backward-style replace tactic generates an equality subgoal for each replacement made:
forall n : nat, (n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1forall n : nat, (n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat(n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat(n + 1) / ((n + 1 - 0) * 1) = 1n: nat(n + 1) / ((n + 1) * 1) = 1apply Nat.div_same; lia. Qed.n: nat(n + 1) / (n + 1) = 1
…which, in forward-style, is equivalent to the following proof that uses assert and rewrite instead:
forall n : nat, (n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1forall n : nat, (n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat(n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat1 ^ 2 = 1n: nat
H: 1 ^ 2 = 1(n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat1 ^ 2 = 1reflexivity.n: nat1 = 1n: nat
H: 1 ^ 2 = 1(n + 1) / ((n + 1 - 0) * 1 ^ 2) = 1n: nat
H: 1 ^ 2 = 1(n + 1) / ((n + 1 - 0) * 1) = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1(n + 1) / ((n + 1 - 0) * 1) = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1(n + 1) / ((n + 1) * 1) = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1
H1: (n + 1) * 1 = n + 1(n + 1) / ((n + 1) * 1) = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1
H1: (n + 1) * 1 = n + 1(n + 1) / (n + 1) = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1
H1: (n + 1) * 1 = n + 11 = 1n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1
H1: (n + 1) * 1 = n + 1n + 1 <> 0lia. Qed.n: nat
H: 1 ^ 2 = 1
H0: n + 1 - 0 = n + 1
H1: (n + 1) * 1 = n + 1n + 1 <> 0
In most cases, we'll prefer the first proof, which is much easier to review.
Destructive tactics
Some tactics that manipulate the context can have a destructive behavior, leading to unprovable goals. For example, induct forgets about the term it is inducting on if it's not a variable:
Inductive even : nat -> Prop := | Even0 : even 0 | EvenSS : forall n, even n -> even (S (S n)).forall n : nat, even (S (S n)) -> even nforall n : nat, even (S (S n)) -> even nn: nat
H: even (S (S n))even n(* Stuck! *) Abort.n: nateven nn, n0: nat
H: even n0
IHeven: even neven n
To work-around that, one can use the remember tactics, which explicitly introduces an equality hypothesis:
forall n : nat, even (S (S n)) -> even nforall n : nat, even (S (S n)) -> even nn: nat
H: even (S (S n))even nn, n': nat
Heqn': n' = S (S n)
H: even n'even nn: nat
Heqn': 0 = S (S n)even nn, n0: nat
Heqn': S (S n0) = S (S n)
H: even n0
IHeven: n0 = S (S n) -> even neven ndiscriminate.n: nat
Heqn': 0 = S (S n)even nn, n0: nat
Heqn': S (S n0) = S (S n)
H: even n0
IHeven: n0 = S (S n) -> even neven nn, n0: nat
H0: n0 = n
H: even n0
IHeven: n0 = S (S n) -> even neven nassumption. Qed.n: nat
IHeven: n = S (S n) -> even n
H: even neven n
Some other examples of destructive tactics include destruct, specialize, subst, and clear.