number-theory/pascal.v
2025-11-26 22:14:40 +11:00

461 lines
16 KiB
Coq

Require Import Ring.
Require Import ArithRing.
(* Coq is like Lisp in that it is so powerful, that every user tends to invent
a whole new programming language using its features. This makes Lisp by
itself an easy tool to misuse, but constrained by an application like CAD or
Emacs, it becomes quite a powerful approach for implementing a small
language.
The goal of this file is to consciously and deliberately create a new
dialect of proof assistant, constrained by the goal that the proofs should
explain themselves as they go, without requiring an IDE, the same way that
real life math proofs explain themselves as you read them.
The two main things that stop an 'idiomatic' Coq proof from being readable
without an IDE, are heavy reliance on proof automation, and hidden state
that varies from line to line in other terse and clever ways. In general the
philosophy of many Coq proofs is, the computer has checked it, so who cares
why it is true? This is not going to be our philosophy.
For an example of both of these weaknesses at their maximum, consider the
following Coq proof: *)
Theorem not_not_law_of_excluded_middle: forall A: Prop,
not (not (A \/ not A)).
Proof.
intros A H.
assert (not A); auto.
Qed.
(* How does this proof work? Who knows! You'll have to run `debug auto`
yourself and see. Lots of effort has been put into this sort of automation,
in fact, the entire proof above could have been replaced with a single
'intuition' tactic, which specializes in intuitionist logic tautologies.
While it is useful to know that Coq *can* go that far with automation, it is
important that we remain tasteful in how we apply it. Proofs are meant to
convince the reader, and ideally to inform them as well. "Look! It type
checks!" is convincing, but not informative.
We will find that proof automation comes in very handy when doing routine
algebraic manipulations, but apart from that, the more explicit we can make
our control flow, the more informative (and thus useful) our proofs will be.
As an example, let's look at a useful but surprisingly involved inductive
proof, that all numbers are either even or odd. First let's define what we
mean by even and odd. *)
(* A number is even if it is equal to some other number, doubled. *)
Definition Even (n: nat): Prop :=
exists k: nat, n = 2 * k.
(* And a number is odd if it is one more than that. *)
Definition Odd (n: nat): Prop :=
exists k: nat, n = 2 * k + 1.
(* Let's try proving a number is even. We'll do zero, since then we have a base
case for induction later. *)
Lemma zero_is_even: Even 0.
Proof.
(* We need to find a k so that 2 * k = 0. Let's try.... 0 *)
exists 0.
(* Now to show that 2 * 0 = 0, we let Coq beta/eta reduce it to 0 = 0, and
then fall back to 'reflexivity', the defining property of Leibniz equality. *)
reflexivity.
Qed.
(* It is annoying that we have to write all of these comments to explain what
the state of the proof assistant is *going* to be. Plus, these comments can
get out of sync with the program, which seems absurd in a language
specifically designed to check such things. Let's make some new tactics to
let us represent this information better. *)
(* This first tactic lets us start such a string of manipulations, by declaring
what the LHS of our goal is, effectively just a comment, but checked. *)
Ltac have expr :=
lazymatch goal with
| |- ?Rel expr ?R =>
idtac
| |- ?Rel ?L ?R =>
fail "Have annotation. Expected" expr "but got" L
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Next, all manipulations are ultimately built out of beta/eta equivalence,
so let's make a tactic to check these equivalences for us. *)
Ltac thatis expr :=
lazymatch goal with
| |- ?Rel ?L ?R =>
unify L expr;
change_no_check (Rel expr R)
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Using thatis makes it easy to annotate how an expression simplifies as it is
reduced, but sometimes we need to show how the RHS simplifies as well, so
let's make a tactic to break up an equation into two parts, showing how each
side equals some simpler intermediate value. *)
Ltac both_sides_equal expr :=
transitivity expr; [ | symmetry].
(* Finally, if we want to be sure that our above annotations are correct, this
tactic will check that the LHS and RHS are not just equivalent, but already
identical. *)
Ltac done :=
lazymatch goal with
| |- ?Rel ?L ?L =>
reflexivity
| |- ?Rel ?L ?R =>
fail "Chain derivation not done. Left:" L "Right:" R
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Let's try again at that simple zero example, but with annotations. *)
Lemma zero_is_even_explicit: Even 0.
Proof.
(* We still need to pick a value for k. *)
exists 0.
(* Now let's prove our goal a little more explicitly. *)
both_sides_equal 0.
-have 0.
done.
-have (2 * 0).
thatis 0.
done.
Qed.
(* This case might be sort of overkill... As one gains/regains familiarity with
DTT, statements like "eq_refl is a proof that 0 = 2 * 0" becomes totally
intuitive, but still, these tactics hopefully explain why.
Now let's do something where this is a little less overkill, showing that if
a number is even, then the next number is odd.
S is the successor, in Peano arithmetic, and inductive proofs. *)
Lemma even_implies_odd: forall n: nat,
Even n -> Odd (S n).
Proof.
intros n IsEven.
destruct IsEven as [k Eq].
(* n is 2 * k, so n+1 is 2 * k + 1, so k is the value we need to prove that
n+1 is odd. 'exists' is a tactic that says a term should be taken as the
term we are trying to prove exists. *)
exists k.
(* Now we actually have to prove that S n = 2 * k + 1, though. *)
have (S n).
rewrite Eq.
have (S (2 * k)).
thatis (1 + 2 * k).
(* This is almost what we want. Let's do a commutativity rewrite. *)
rewrite (PeanoNat.Nat.add_comm _ _).
have (2 * k + 1).
done.
Qed.
(* Having these tactics definitely makes the control flow easier to follow, but
it would be nice if we didn't have to appeal to the laws of associativity
and commutativity every time our goal was expressed slightly differently to
what we expected. Let's take some of those powerful proof automation tools,
and use them for this very narrow task of algebraic manipulation. *)
Ltac rearrange expr :=
lazymatch goal with
| |- ?Rel ?L ?R =>
transitivity expr; [> ring | ]
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Further, manipulating existence proofs using exists and destruct is a bit
hard to follow. Let's make some tactics for declaring what we know, and what
we have to prove, as each of these things change from line to line. *)
(* If our goal isn't a relation yet, we might still want to declare it. *)
Ltac havegoal expr :=
lazymatch goal with
| |- expr =>
idtac
| |- ?Other =>
fail "Expected goal to be" expr "but it was" Other
end.
(* The place where we learn new equations is often quite far from where we
use them. This tactic lets us state in-context what hypothesis we are
using, and defer choosing a name for it until then as well. *)
Ltac weknow newid P :=
lazymatch goal with
| proof: P |- ?Goal =>
rename proof into newid
| |- ?Goal =>
fail "Knowledge assertion. Cannot find proof of" P
end.
Tactic Notation "weknow" ident(newid) ":" constr(P) :=
weknow newid P.
(* We're going to need these for odd_implies_even, as that case is harder! *)
Lemma odd_implies_even: forall n: nat,
Odd n -> Even (S n).
Proof.
intros.
weknow IsOdd: (Odd n).
destruct IsOdd as [k].
havegoal (Even (S n)).
weknow OddEq: (n = 2 * k + 1).
(* When we do goal manipulations like 'exists', let's annotate the new goal
on the same line using the semicolon separator. *)
exists (S k); havegoal (S n = 2 * S k).
both_sides_equal (2 * k + 2).
-have (S n).
(* Let's also annotate rewrites on the same line, if it will fit. *)
rewrite OddEq; have (S (2 * k + 1)).
(* We should get used to S as a first-class operation, since this is Peano
arithmetic, at the end of the day. But for now we will get rid of the S
and apply algebraic manipulations to more traditional expressions. *)
thatis (1 + 2 * k + 1).
(* Now let's get Coq to prove our algebra for us! *)
rearrange (2 * k + 2).
done.
-have (2 * S k).
(* Again, get rid of the S. *)
thatis (2 * (1 + k)).
(* And rearrange. *)
rearrange (2 * k + 2).
done.
Qed.
(* You might see where this is going now. We have all the pieces to build an
inductive proof! *)
Theorem even_or_odd: forall n: nat,
Even n \/ Odd n.
Proof.
intro n.
induction n.
(* To prove something inductively, we have to prove it is true for 0, and
then that it is true for any number of the form S n. *)
*havegoal (Even 0 \/ Odd 0).
left; havegoal (Even 0).
exact zero_is_even.
*havegoal (Even (S n) \/ Odd (S n)).
weknow Ind: (Even n \/ Odd n).
(* To prove that S n is even or odd, we need to know which case we are in
when it comes to n being even or odd. *)
destruct Ind.
+weknow IsEven: (Even n).
(* We can use the 'left' and 'right' tactics to choose how we are going to
prove a dysjunction. *)
right; havegoal (Odd (S n)).
(* We are appealing to a lemma, so get the Curry-Howard proof object for
that lemma, and apply it to produce the 'exact' proof we need. *)
exact (even_implies_odd n IsEven).
+weknow IsOdd: (Odd n).
(* Very similar logic on the other side. *)
left; havegoal (Even (S n)).
exact (odd_implies_even n IsOdd).
Qed.
(* Wasn't that a fun and pleasant adventure?
So we see that more explicit control flow sets us up for quite nice proofs,
where we can see that the goal follows by simple algebra, algebra which Coq
is prepared to do for us.
Agda and Idris programmers will recognize that Even and Odd could have been
jerry-rigged so that the `ring` tactics weren't required at all, and while
we will be exploiting that sort of trick to simplify things when they get
hard, it's useful to embrace these un-optimized definitions when trying to
demonstrate Coq tactics. *)
(* Now, finally, let's get on with some number theory! First let's look at
Pascal's triangle, since understanding that turns out to be essential to
understanding how to find points on elliptic curves.
Our way of indexing Pascal's triangle is going to be a little weird, though;
in order to avoid having to make special cases for all the zeros to the left
and right of the triangle, we will index the triangle in diagonal strips, so
e.g.
pascal 0 _ = 1
pascal 1 k = k
pascal 2 k = k*(k+1)/2
pascal 3 k = k*(k+1)*(k+2)/6
etc.
To do this without getting dizzy over nested cases, let's get dizzy over
higher order functions instead. Let's define a function that takes a
sequence, and returns a sequence representing the partial sums of that
sequence. *)
Fixpoint partial_sum (seq: nat -> nat) (count: nat): nat :=
match count with
| O => 0
| S count' => partial_sum seq count' + seq count'
end.
(* But Triangular numbers aren't the sum of 0 or more numbers, they are the sum
of one or more numbers! So let's also define a 'series' variation. *)
Definition series (seq: nat -> nat) (index: nat): nat :=
partial_sum seq (1 + index).
Fixpoint pascal (r: nat): nat -> nat :=
match r with
| 0 => fun _ => 1
| S r' => series (pascal r')
end.
(* Now the first 'row' of our function is all 1 by definition, but it is useful
to show that the first 'column' is all 1 as well. *)
Lemma pascal_0: forall r: nat,
pascal r 0 = 1.
Proof.
intro r.
induction r.
*have (pascal 0 0).
thatis 1.
done.
*have (pascal (S r) 0).
thatis (series (pascal r) 0).
thatis (pascal r 0).
(* Let's stop this chain reasoning here, and look at what our goal is now. *)
havegoal (pascal r 0 = 1).
weknow Ind: (pascal r 0 = 1).
exact Ind.
Qed.
(* Next let's define some combinatoric functions, and see if we can't prove how
they relate to the recursive formula above. *)
Fixpoint factorial (n : nat): nat :=
match n with
| S n' => n * factorial n'
| O => 1
end.
Fixpoint prod_up (k r: nat): nat :=
match k with
| 0 => 1
| S k' => prod_up k' (1 + r) * r
end.
(* Often this permutation prod_up is notated as factorial (k + r) / factorial k,
but we don't have division in peano arithmetic, so let's show this relation
through multiplication instead. *)
Lemma prod_up_ratio: forall k r: nat,
prod_up k (1 + r) * factorial r = factorial (k + r).
Proof.
intros k.
induction k; intro r.
*cbn.
havegoal (factorial r + 0 = factorial r).
ring.
*have (prod_up (S k) (1 + r) * factorial r).
thatis (prod_up k (2 + r) * (1 + r) * factorial r).
weknow Ind: (forall r': nat, prod_up k (1 + r') * factorial r' = factorial (k + r')).
rearrange (prod_up k (2 + r) * ((1 + r) * factorial r)).
thatis (prod_up k (1 + (1 + r)) * factorial (1 + r)).
rewrite (Ind (1 + r)); have (factorial (k + (1 + r))).
thatis (factorial (k + S r)).
(* Use one of those manual manipulations, since the term we are manipulating
is inside of a function call, so ring will get confused. *)
rewrite <- (plus_n_Sm k r); have (factorial (S (k + r))).
thatis (factorial (S k + r)).
done.
Qed.
(* If we let r = 0 then we even show that the factorial is a special case of
this product function. *)
Lemma factorial_up: forall n: nat,
prod_up n 1 = factorial n.
Proof.
intro n.
have (prod_up n 1).
rearrange (prod_up n 1 * 1).
thatis (prod_up n (1 + 0) * factorial 0).
rewrite (prod_up_ratio n 0); have (factorial (n + 0)).
rewrite <- (plus_n_O); have (factorial n).
done.
Qed.
(* Another trick that is implicit in pen-and-paper notation, that we have to be
explicit about in code, is the fact that we can pull terms off of either
side of this product, to do algebraic manipulations with. *)
Lemma prod_up_down: forall k r: nat,
prod_up (1 + k) r = (k + r) * prod_up k r.
Proof.
intro k.
induction k; intro r.
*cbn.
havegoal (r + 0 = r * 1).
ring.
*have (prod_up (1 + S k) r).
thatis (prod_up (1 + k) (1 + r) * r).
weknow Ind: (forall r': nat, prod_up (1 + k) r' = (k + r') * prod_up k r').
rewrite (Ind (1 + r)).
have ((k + (1 + r)) * prod_up k (1 + r) * r).
rearrange ((S k + r) * (prod_up k (1 + r) * r)).
thatis ((S k + r) * prod_up (S k) r).
done.
Qed.
(* Now let's get to the theorem! I haven't broken this one up at all. Turn your
font up or something. *)
Theorem pascal_choose: forall r k: nat,
pascal r k * factorial k = prod_up k (S r).
Proof.
intro r.
induction r; intro k.
*cbn.
ring_simplify.
exact (eq_sym (factorial_up k)).
*induction k.
**cbn.
ring_simplify.
exact (pascal_0 r).
**have (pascal (S r) (S k) * factorial (S k)).
thatis ((pascal (S r) k + pascal r (S k)) * factorial (S k)).
weknow IndR: (forall k': nat, pascal r k' * factorial k' = prod_up k' (S r)).
(* We have a pascal r (S k) and a factorial (S k), let's get them together. *)
rearrange (pascal (S r) k * factorial (S k) + pascal r (S k) * factorial (S k)).
rewrite (IndR (S k)).
have (pascal (S r) k * factorial (S k) + prod_up (S k) (S r)).
(* Good. Now, look for similar to apply to pascal (S r) k *)
weknow IndK: (pascal (S r) k * factorial k = prod_up k (S (S r))).
(* So we need factorial k. That can be arranged. *)
thatis (pascal (S r) k * (S k * factorial k) + prod_up (S k) (S r)).
rearrange (S k * (pascal (S r) k * factorial k) + prod_up (S k) (S r)).
rewrite (IndK).
have (S k * prod_up k (S (S r)) + prod_up (S k) (S r)).
(* This is good. All the factorial pascal stuff is gone. Now we just need
to factorise these big products. They are pretty close already. *)
thatis (S k * prod_up k (S (S r)) + prod_up k (S (S r)) * S r).
(* Let's rewrite these S's. *)
thatis ((1 + k) * prod_up k (2 + r) + prod_up k (2 + r) * (1 + r)).
(* And collect! *)
rearrange ((k + (2 + r)) * prod_up k (2 + r)).
(* And look at that, the coefficients add up to the next factor. *)
rewrite <- (prod_up_down k (2 + r)).
have (prod_up (1 + k) (2 + r)).
thatis (prod_up (S k) (S (S r))).
done.
Qed.