The logical toolkit¶
Open core.alg and scroll past the equality rules — the middle of the file is a
little library of natural deduction: one pair of rules for each connective, one to
build it and one (or more) to use it. Learn to spot that pattern and the
whole module falls into place.
We’ll state each fact about abstract propositions A, B, C : Prop — so
the logic stays in focus, with no equations to distract us. When a proof needs to
start from some assumption, we take it as a lemma parameter: writing
x := A in a lemma’s parameter list means “x is a proof of A,” and you
discharge a goal that matches it with by x. Keep that reading in mind — half the
tour is just plugging assumptions into holes.
Conjunction: ∧¶
To build a conjunction you need both halves. and_intro takes a proof of A
and a proof of B and hands back A ∧ B — two premises, so we branch with
cases and close each with the matching assumption:
import core(and_intro);
lemma both(A B : Prop, x := A, y := B)
⊢ A ∧ B;
proof
by and_intro(A, B) cases
case ⊢ A; by x; qed;
case ⊢ B; by y; qed;
qed;
qed;
To use a conjunction, take it apart. and_left turns A ∧ B back into A
(and and_right into B). One premise — the conjunction — so we continue with
then, and discharge it with the both we were handed:
import core(and_left);
lemma just_left(A B : Prop, both := A ∧ B)
⊢ A;
proof
by and_left(A, B) then ⊢ A ∧ B; by both;
qed;
Notice the rhythm: by and_left(A, B) says “I’m going to get A out of the
conjunction A ∧ B,” and the then goal is the whole conjunction you now owe a
proof of — which both supplies.
Your turn
Conjunction doesn’t care about order. Given a proof of A ∧ B, prove B ∧ A.
import core(and_intro, and_left, and_right);
lemma and_comm(A B : Prop, both := A ∧ B)
⊢ B ∧ A;
proof
by wip(?goal);
wip;
Hint
and_intro(B, A) splits the goal into ⊢ B and ⊢ A — two goals, so
cases. Get the B half with and_right(A, B) and the A half with
and_left(A, B), each then-ing on A ∧ B and closing by both.
Disjunction: ∨¶
Building a disjunction only needs one side. or_intro_left proves A ∨ B
from A (and or_intro_right from B) — a single premise, so then:
import core(or_intro_left);
lemma pick_left(A B : Prop, x := A)
⊢ A ∨ B;
proof
by or_intro_left(A, B) then ⊢ A; by x;
qed;
Using a disjunction is the interesting one, because you don’t know which side
holds. or_elim makes you prove your goal both ways — once assuming A,
once assuming B. Three premises (the disjunction plus the two branches), so
three case s. Here’s disjunction’s own commutativity:
import core(or_elim, or_intro_left, or_intro_right);
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;
Two new things here. First, where do P and Q come from? A rule that lets
you assume something names the assumption after its own premise. In core.alg,
or_elim ‘s branches are written P := P ⊢ R and Q := Q ⊢ R, so inside the
left branch your new hypothesis is called P and in the right branch it’s Q.
You discharge it exactly like a lemma parameter — by P. Second, the branches
build the flipped disjunction with the intro rules we just met.
Your turn
Build the right-hand disjunct this time.
import core(or_intro_right);
lemma pick_right(A B : Prop, y := B)
⊢ A ∨ B;
proof
by wip(?goal);
wip;
Hint
or_intro_right(A, B) proves A ∨ B from ⊢ B — the right side. One
premise means then ⊢ B;, and you already hold a proof of B: by y.
Implication: ⇒¶
To build an implication A ⇒ B you assume A and prove B.
implication_intro introduces the antecedent as a hypothesis — named P, after
its premise P := P ⊢ Q — and asks you to reach B. The smallest example is
the identity A ⇒ A, where the assumption is the goal:
import core(implication_intro);
lemma id(A : Prop)
⊢ A ⇒ A;
proof
by implication_intro(A, A) then P := A ⊢ A; by P;
qed;
To use an implication, feed it its antecedent. implication_elim is plain modus
ponens: from A ⇒ B and A, conclude B. Two premises, cases, both
discharged from assumptions we were handed:
import core(implication_elim);
lemma mp(A B : Prop, f := A ⇒ B, x := A)
⊢ B;
proof
by implication_elim(A, B) cases
case ⊢ A ⇒ B; by f; qed;
case ⊢ A; by x; qed;
qed;
qed;
Negation and falsehood¶
Negation is really implication in disguise: ¬A means “A leads to
absurdity.” So proving ¬A from a proof that A ⇒ False is almost a
tautology — negation_intro assumes A (as P), and we run the implication
to reach False:
import core(negation_intro, implication_elim);
lemma neg_from_imp(A : Prop, f := A ⇒ False)
⊢ ¬A;
proof
by negation_intro(A) then P := A ⊢ False;
by implication_elim(A, False) cases
case ⊢ A ⇒ False; by f; qed;
case ⊢ A; by P; qed;
qed;
qed;
And once you have False, you have everything — false_elim proves any
proposition at all (the principle of explosion):
import core(false_elim);
lemma explosion(A : Prop, bad := False)
⊢ A;
proof
by false_elim(A) then ⊢ False; by bad;
qed;
The rule that produces False is negation_elim: from A and ¬A — a
contradiction — it derives False (two premises, cases). Chain it into
false_elim and a contradiction proves anything at all.
Your turn
Put the last two together: from a proof of A and a proof of ¬A, derive a
completely unrelated C.
import core(false_elim, negation_elim);
lemma clash(A C : Prop, x := A, nx := ¬A)
⊢ C;
proof
by wip(?goal);
wip;
Hint
Start with by false_elim(C) then ⊢ False; — now you only owe False.
Reach it with negation_elim(A), whose two cases are ⊢ A (close
by x) and ⊢ ¬A (close by nx).
With the connectives in hand, the only thing left in core is the quantifiers —
and they’re next.