Proving with auxiliary lemmas¶
By now your proofs chain a fair number of by steps. Real developments get
bigger still — and the cure is the same one you’d reach for in any program: pull
a self-contained piece out, give it a name, and reuse it. In Algae that piece is
a lemma, and the payoff is direct:
Once a lemma’s
qedchecks, it becomes a fact — you invoke it by name withby, exactly like an axiom or an inference rule.
Its parameters become the arguments you pass; its goal becomes the conclusion it discharges. Because a proved lemma has nothing left to prove, applying it closes the goal outright — zero subgoals, just like an axiom.
A proof becomes a fact¶
Here’s a small helper — conjunction is commutative — and a second lemma that uses it:
import core(and_intro, and_left, and_right, implication_intro);
lemma and_comm(A B : Prop, both := A ∧ B)
⊢ B ∧ A;
proof
by and_intro(B, A) cases
case ⊢ B; by and_right(A, B) then ⊢ A ∧ B; by both; qed;
case ⊢ A; by and_left(A, B) then ⊢ A ∧ B; by both; qed;
qed;
qed;
lemma and_comm_imp(A B : Prop)
⊢ (A ∧ B) ⇒ (B ∧ A);
proof
by implication_intro(A ∧ B, B ∧ A)
then P := A ∧ B ⊢ B ∧ A;
by and_comm(A, B, P);
qed;
Look at the last line. and_comm’s signature is (A B : Prop, both := A ∧ B),
so invoking it takes three arguments: the two propositions A and B, and a
proof of A ∧ B — here the hypothesis P that implication_intro just
handed us. The lemma’s conclusion, B ∧ A, matches the goal, so by and_comm(A,
B, P) finishes the branch on its own. A term parameter takes a term; a
:= parameter takes a proof; a proved lemma behaves like any other rule with
zero premises.
Prove once, reuse forever¶
The real reason to factor a lemma out is that some facts are expensive to
prove. The natural-number fact ∀ n. n + 0 = n needs a full induction (you met
it in Induction and friends); it lives, already proved, in nat.alg as
add_zero_right. You never want to redo that induction. Instead, import it
and instantiate it at whatever point you need:
import nat;
import core(forall_elim);
lemma add_zero_at(a : Nat)
⊢ a + 0 = a;
proof
by forall_elim(Nat, _ + 0 = _, a)
then ⊢ ∀ (n : Nat) st n + 0 = n;
by add_zero_right;
qed;
add_zero_right takes no arguments — it’s a closed universal fact — so by
add_zero_right; discharges the ∀ goal directly, and forall_elim peels it
down to the instance a + 0 = a. The whole stdlib is built this way: each
module’s harder theorems lean on the simpler lemmas below them.
Note
Order doesn’t matter within a unit. The checker reads every declaration in the file before it checks any proof, so a lemma may cite one defined further down (or a mutually-useful pair may cite each other). Idiomatic style is still bottom-up — helpers first — because it reads like the dependency order.
Your turn
Disjunction is commutative too — and it’s already proved for you below as
or_comm. Use it to prove that A ∨ B implies B ∨ A.
import core(or_elim, or_intro_left, or_intro_right, implication_intro);
lemma or_comm(A B : Prop, d := A ∨ B)
⊢ B ∨ A;
proof
by or_elim(A, B, B ∨ A) cases
case ⊢ A ∨ B; by d; qed;
case P := A ⊢ B ∨ A; by or_intro_right(B, A) then ⊢ A; by P; qed;
case Q := B ⊢ B ∨ A; by or_intro_left(B, A) then ⊢ B; by Q; qed;
qed;
qed;
lemma or_comm_imp(A B : Prop)
⊢ (A ∨ B) ⇒ (B ∨ A);
proof
by wip(?goal);
wip;
Hint
Mirror and_comm_imp. Start with by implication_intro(A ∨ B, B ∨ A),
which leaves then P := A ∨ B ⊢ B ∨ A;. Now or_comm’s conclusion is
exactly B ∨ A — feed it the two propositions and your hypothesis:
by or_comm(A, B, P);.
That’s the whole trick. A lemma is a proof you get to name and a fact you get to reuse — the same duality that makes the standard library, and any development you build on top of it, scale.