Lab 1: Debrief
- Author
Dario Halilovic, Clément Pit-Claudel
From Stdlib Require Import Lia Arith String List. Open Scope N_scope.
Notes on Rocq tactics
apply vs rewrite
These two tactics are almost never interchangeable.
applyimplements the following rule, which means “to proveB, it's enough to proveAandA → B”rewriteimplements a different rule that means “to proveC, it's enough to provex = yandCwith allxreplaced byy”:
Use apply when the conclusion of a hypothesis matches the goal:
H: … → conclusion =================== conclusion
Use rewrite when a hypothesis is an equality about a variable mentionned in the goal:
H: a = … ================== … a …
Here is a concrete example:
forall (A B : Type) (f : A -> B) (l : list A), Datatypes.length (map f l) = Datatypes.length lforall (A B : Type) (f : A -> B) (l : list A), Datatypes.length (map f l) = Datatypes.length lA, B: Type
f: A -> B
a: A
l: list A
IHl: Datatypes.length (map f l) = Datatypes.length lS (Datatypes.length (map f l)) = S (Datatypes.length l)
apply doesn't work out of the box, because the goal doesn't match; we can either use rewrite and equality or f_equal and apply:
reflexivity. Qed.A, B: Type
f: A -> B
a: A
l: list A
IHl: Datatypes.length (map f l) = Datatypes.length lS (Datatypes.length l) = S (Datatypes.length l)
Here's how it looks with apply:
forall (A B : Type) (f : A -> B) (l : list A), Datatypes.length (map f l) = Datatypes.length lforall (A B : Type) (f : A -> B) (l : list A), Datatypes.length (map f l) = Datatypes.length lA, B: Type
f: A -> B
a: A
l: list A
IHl: Datatypes.length (map f l) = Datatypes.length lS (Datatypes.length (map f l)) = S (Datatypes.length l)apply IHl. Qed.Datatypes.length (map f l) = Datatypes.length l
Implicit quantification in lemma statements
The following two lemmas statements mean the same thing, but they behave slightly differently:
forall i j k : nat, i < j -> j < k -> i < kforall i j k : nat, i < j -> j < k -> i < kforall j k : nat, 0 < j -> j < k -> 0 < ki: nat
IHi: forall j k : nat, i < j -> j < k -> i < kforall j k : nat, S i < j -> j < k -> S i < ki: nat
IHi: forall j k : nat, i < j -> j < k -> i < kforall j k : nat, S i < j -> j < k -> S i < kAbort.i, j, k: nati < j -> j < k -> i < ki, j, k: nati < j -> j < k -> i < kj, k: nat0 < j -> j < k -> 0 < ki, j, k: nat
IHi: i < j -> j < k -> i < kS i < j -> j < k -> S i < ki, j, k: nat
IHi: i < j -> j < k -> i < kS i < j -> j < k -> S i < kAbort.
The key difference is that in the second version, i, j, and k are already “introduced”, as if we had run intros, and as a result the induction hypothesis doesn't get forall-quantified over j and k.
induction vs destruct
If you want to perform case analysis on some expression x, use destruct x and not induction x.
While the latter will essentially do the same for non-recursive inductive types, the former is much more idiomatic and does what you intended with recursive inductive types.
With recursive inductive types, using induction x would add unnecessary induction hypotheses to the environment.
Consider the following example:
forall n : nat, n > 0 -> n - 1 < nforall n : nat, n > 0 -> n - 1 < n
We want to perform case analysis on n to treat the case n = 0 separately.
n: natn > 0 -> n - 1 < n0 > 0 -> 0 - 1 < 0n: nat
IHn: n > 0 -> n - 1 < nS n > 0 -> S n - 1 < S n0 > 0 -> 0 - 1 < 0ABS: 0 > 00 - 1 < 0ABS: 0 < 00 - 1 < 0ABS: 0 < 0Falseassumption.ABS: 0 < 00 < 0n: nat
IHn: n > 0 -> n - 1 < nS n > 0 -> S n - 1 < S n
Performing induction on n adds an unnecessary induction hypothesis to the environment.
Abort.forall n : nat, n > 0 -> n - 1 < nforall n : nat, n > 0 -> n - 1 < nn: natn > 0 -> n - 1 < n0 > 0 -> 0 - 1 < 0n: natS n > 0 -> S n - 1 < S n0 > 0 -> 0 - 1 < 0ABS: 0 > 00 - 1 < 0ABS: 0 < 00 - 1 < 0ABS: 0 < 0Falseassumption.ABS: 0 < 00 < 0n: natS n > 0 -> S n - 1 < S n
Here, we do not have an unnecessary induction hypothesis in the environment.
n: natS n - 1 < S nn: natn - 0 < S nn: natn < S nreflexivity. Qed.n: natS n <= S n
Writing better proofs
Consider using bullets or braces to structure your proofs. You can use -, +, and * in order, and if you're using Emacs, they'll be indented automatically.
forall A B : Prop, A <-> B -> B -> (((A /\ A) /\ A) /\ A /\ A) /\ Aforall A B : Prop, A <-> B -> B -> (((A /\ A) /\ A) /\ A /\ A) /\ AA, B: Prop
H: A <-> B
H0: B(((A /\ A) /\ A) /\ A /\ A) /\ AA, B: Prop
H: A <-> B
H0: B((A /\ A) /\ A) /\ A /\ AA, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: B((A /\ A) /\ A) /\ A /\ AA, B: Prop
H: A <-> B
H0: B(A /\ A) /\ AA, B: Prop
H: A <-> B
H0: BA /\ AA, B: Prop
H: A <-> B
H0: B(A /\ A) /\ AA, B: Prop
H: A <-> B
H0: BA /\ AA, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BA /\ AA, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BAapply H; assumption.A, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BAapply H; assumption. }A, B: Prop
H: A <-> B
H0: BAapply H; assumption.A, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BA /\ AA, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BAapply H; assumption.A, B: Prop
H: A <-> B
H0: BAA, B: Prop
H: A <-> B
H0: BAapply H; assumption. }A, B: Prop
H: A <-> B
H0: BAapply H; assumption. Qed.A, B: Prop
H: A <-> B
H0: BA
Note that the indentation naturally follows the structure of the proof. If you however find yourself needing more than 3 levels of nesting, you should introduce sublemmas—“bullet hell” is a real thing!
When structuring your proofs, you should also aim to factor out common tactics as often as possible using the semi-colon (;) tactical. This can greatly shorten your proofs when all cases are similar enough.
If your cases slightly differ by one tactic, make use of the square brackets ([ | ]) to dispatch tactics to each case:
forall A B : Prop, A \/ B -> B \/ Aforall A B : Prop, A \/ B -> B \/ AA, B: Prop
H: AB \/ AA, B: Prop
H: BB \/ AA, B: Prop
H: AB \/ Aassumption.A, B: Prop
H: AAA, B: Prop
H: BB \/ Aassumption. Qed.A, B: Prop
H: BB
…can be rewritten as:
forall A B : Prop, A \/ B -> B \/ Aintros; destruct H; [ right | left ]; assumption. Qed.forall A B : Prop, A \/ B -> B \/ A
Bonus trick: when chaining tactics with ;, if only the first goal differs from the others, use [ tac1 | .. ] to apply tac1 to the first goal only.
Manually selecting subgoals
When you have many goals, we do not recommend selecting subgoals manually (don't write 1,3,9,57: reflexivity).
It's inelegant, hard to read, and brittle (if goals get ordered differently the wrong goals will get selected).
Instead, if you have many goals, use automation! See this exercise for an example.
Unnecessary steps
Here is a (non-exhaustive) list of steps that can be shortened or removed:
simplandintrosare unnecessary beforereflexivity.intros xis redundant beforedestruct/induction xifxwas aforall-quantified variable.destruct x; destruct ycan be replaced withdestruct x, y.try repeatis equivalent torepeat:repeatnever fails!
Avoid automatically generated names
Always choose names for your variables/hypotheses: this makes it easier for everyone (including yourself!) to follow your proof at a glance. For example:
forall n m : nat, n <= m -> n * n <= m * mforall n m : nat, n <= m -> n * n <= m * mn: nat
H: n <= 0n * n <= 0 * 0n, m: nat
IHm: n <= m -> n * n <= m * m
H: n <= S mn * n <= S m * S mn: nat
H: n <= 0n * n <= 0 * 0n: nat
H: n = 0n * n <= 0 * 0constructor.n: nat
H: n = 00 * 0 <= 0 * 0n, m: nat
IHm: n <= m -> n * n <= m * m
H: n <= S mn * n <= S m * S mn, m: nat
IHm: n <= m -> n * n <= m * m
H: n <= m \/ n = S mn * n <= S m * S mn, m: nat
IHm: n <= m -> n * n <= m * m
H: n <= mn * n <= S m * S mn, m: nat
IHm: n <= m -> n * n <= m * m
H: n = S mn * n <= S m * S mlia.n, m: nat
IHm: n <= m -> n * n <= m * m
H: n <= mn * n <= S m * S mn, m: nat
IHm: n <= m -> n * n <= m * m
H: n = S mn * n <= S m * S mconstructor. Qed.n, m: nat
IHm: n <= m -> n * n <= m * m
H: n = S mS m * S m <= S m * S m
…is not easy to read (what's that H hypothesis all about?). Instead, giving names to each hypothesis helps a lot:
forall n m : nat, n <= m -> n * n <= m * mforall n m : nat, n <= m -> n * n <= m * mn, m: nat
Hn_le_m: n <= mn * n <= m * mn: nat
Hn_le_m: n <= 0n * n <= 0 * 0n, m: nat
Hn_le_m: n <= S m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mn: nat
Hn_le_m: n <= 0n * n <= 0 * 0n: nat
Hn_le_m: n = 0n * n <= 0 * 0constructor.n: nat
Hn_le_m: n = 00 * 0 <= 0 * 0n, m: nat
Hn_le_m: n <= S m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mn, m: nat
Hn_le_m: n <= m \/ n = S m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mn, m: nat
Hn_lt_m: n <= m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mn, m: nat
Hn_equals_m: n = S m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mlia.n, m: nat
Hn_lt_m: n <= m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mn, m: nat
Hn_equals_m: n = S m
IHm: n <= m -> n * n <= m * mn * n <= S m * S mconstructor. Qed.n, m: nat
Hn_equals_m: n = S m
IHm: n <= m -> n * n <= m * mS m * S m <= S m * S m
Some common tactics, such as destruct, induction or intros, have variants for introducing your own names. A few useful examples:
destruct H as [HcaseA | HcaseB](disjunction case)destruct H as [HA HB](conjunction case)induction n as [ | IHn]intros (HA & HB & HC)
For more examples, have a look at the reference manual.