number-theory/pascal.v
Jarvis Carroll 48136669a9 Refactor even/odd stuff
It's now broken down into multiple lemmas, and my weird tactics are
introduced along the way.

I also changed some style things later on.
2025-11-22 02:03:51 +11:00

423 lines
14 KiB
Coq

Require Import Ring.
Require Import ArithRing.
(* Coq is a powerful language, that can search for proofs automatically, and
then verify them automatically. This results in proofs like *)
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. If you are trying to
prove something formally, it is usually because you know that it works in
some cases, and what you really want to know is *why* those cases work.
In particular, in writing proofs about cryptography and number theory, we
will happily indulge in proof automation to expand brackets and collect like
terms, but the 'control flow' of our proof should be explicit. We want to
know what cases we are considering, and why, and what the key insights are
that make those mundane algebraic manipulations useful.
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.
(* Similarly, this tactic lets us declare what the RHS is, before we do
strong manipulations like ring. *)
Ltac want expr :=
lazymatch goal with
| |- ?Rel ?L expr =>
idtac
| |- ?Rel ?L ?R =>
fail "Want annotation. Expected" expr "but got" R
| |- ?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.
(* 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 fill our goal a little more explicitly. *)
want (2 * 0).
have 0.
(* So that is our RHS and LHS... How are we ever going to connect them?
Well let's compute our LHS a little! *)
thatis (2 * 0).
(* And that matches our goal! Look! *)
done.
Qed.
(* Probably overkill for that case, and frankly unintuitive when all the
computation happens on the RHS. Oh well, let's try something more involved. *)
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, and we need to show that n+1 = 2*k' + 1, I wonder what k' can
be? *)
exists k.
want (2 * k + 1).
have (S n).
(* Now let's do some real tactical proving! The equation that proves n is
even can now be used to manipulate our term. *)
rewrite Eq.
(* Now let's see what that did to our LHS. *)
have (S (2 * k)).
(* This isn't quite our LHS. Let's dig into some natural number identities. *)
rewrite (plus_n_O (2 * k)) at 1.
have (S (2 * k + 0)).
rewrite (plus_n_Sm (2 * k) 0).
have (2 * k + 1).
done.
Qed.
(* Rewriting n into 2 * k with these tactics was nice, but the identities at
the end sucked. Let's bring back a little more automation; just enough to do
algebra, but without hiding any important control flow. *)
Ltac rearrange expr :=
lazymatch goal with
| |- ?Rel ?L ?R =>
transitivity expr; [> ring | ]
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Before we try using this one, let's also define some things that will help
us annotate these Even/Odd/exists manipulations. *)
(* 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).
(* From now on let's do the goal manipulation, and then justify it afterwards
by annotating what our goal is now. *)
exists (S k); havegoal (S n = 2 * S k).
have (S n).
(* Let's also combine rewrites with the new expression, if they'll fit on one line. *)
rewrite OddEq; have (S (2 * k + 1)).
(* Now let's write the algebra we would write on paper, and let Coq justify it. *)
rearrange (2 * S k).
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.
*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).
destruct Ind.
+weknow IsEven: (Even n).
right; havegoal (Odd (S n)).
exact (even_implies_odd n IsEven).
+weknow IsOdd: (Odd n).
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, but for this simple example, having suboptimal definitions was useful
as it motivated us to learn how to make Coq do the boring parts for us. *)
(* 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).
want 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)).
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.