number-theory/pascal.v
Jarvis Carroll 4d8d3e5f81 Pascal's Triangle Theorem
I quite like this little paradigm I have come up with here.

I have proved that the combinatoric choice definition and the
recursive Pascal's triangle definition agree.
2025-11-22 00:31:51 +11:00

419 lines
15 KiB
Coq

(* 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, 2 * k = n.
(* And a number is odd if it is one more than that. *)
Definition Odd (n: nat): Prop :=
exists k: nat, 2 * k + 1 = n.
(* Now let's get into the proof. We are going to try to use automation more
tastefully this time, to only do mundane manipulations. Let's import the
'Ring' tactics, which is where Coq defines how to automatically simplify
expressions built out of + and * operations. *)
Require Import Ring.
Require Import ArithRing.
(* Now let's get into the proof. This proof is chosen because it is a good
demonstration of a lot of the features, strengths, and weaknesses of Coq,
all in one place. It will be easier to follow than an 'auto' one-liner, but
it still features a lot of the hidden state that is typical of Coq proofs.
Feel free to read through by yourself, or to step through with the IDE to
see what that hidden state actually looks like on each line, or once you get
the point, that hidden state is hard to follow, feel free to move on to
where we try and address exactly that, by making the state more explicit. *)
Theorem even_or_odd: forall n: nat,
Even n \/ Odd n.
Proof.
intro n.
(* This is an inductive proof. We prove 0 is even or odd, then 1 is even or
odd, then 2 is even or odd, etc. *)
induction n.
(* First prove 0 is even or odd... Let's go with even! *)
*left.
(* We have to choose k so that 2*k = 0. *)
exists 0.
(* Now we have to actually prove that 2*k = 0. In DTT this simplifies to
0 = 0, so we can use reflexivity on 0. *)
reflexivity.
(* Now the inductive case. If n is even or odd, we want to show that n+1 is
even or odd. Check which case we are in first. *)
*destruct IHn as [IsEven | IsOdd].
(* The first case is that n is even, in which case n+1 will be odd. *)
+right.
(* So, we know 2*k = n, and we want to show 2*k' + 1 = n + 1... Clearly
k' = k. *)
destruct IsEven as [k Eq].
exists k.
(* Now Coq gives us the goal 2*k + 1 = n + 1... Let's substitute n in. *)
rewrite <- Eq.
(* Now the goal is 2*k + 1 = 1 + 2 * k. This is clearly true by the
algebraic properties of + and *. We use the 'ring' tactic, even though
natural numbers are only a semiring. The logic still works! *)
ring.
(* That handles the even case. Now, if n is odd, we want to show that n+1 is
even. *)
+left.
(* n is odd, i.e. n = 2*k+1, and we want to show n+1 is even, i.e.
n+1 = 2*k'. Choose k' = k + 1. *)
destruct IsOdd as [k Eq].
exists (k + 1).
(* The goal is 2*(k+1) = 1+n. Substitute n in. *)
rewrite <- Eq.
(* Now the goal is 2*(k+1) = 1 + (2*k + 1)... Let Coq do the algebra. *)
ring.
Qed.
(* 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
learning that sort of trick is immensely productive for understanding DTT,
it is not very useful for understanding number theory.
The problem remains, however, that we had to write comments to track what
the state of the program was going to be. That is confusing, even with
comments, and who knows if the comments are up to date! They aren't checked,
despite us being in a language specifically designed to check things like
that. Let's make some tactics that let us declare what algebraic expression
we think we are working with, so that our proofs become just a little more
self-documenting.
A very common pattern when proving relations in real math proofs, is to
start with one side of the relation, and to manipulate it bit by bit, until
it matches the other side that we wanted. As long as each manipulation
proves a transitive relation, all those transitive steps can be composed
together into a bigger proof. Let's make some tactics that let us do that,
when our goal is some relation L = R, or Rel L R more generally. *)
(* 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 algebraic manipulations in type theory must be shown to follow
from the lambda calculus rules of beta/eta reduction, or similar, so let's
make a tactic that lets us manipulate our LHS using whatever beta/eta rules
Coq supports. *)
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.
(* If beta/eta isn't enough, then we can often specify an intermediate
expression, and get Coq to step there using ring algebra. *)
Ltac rearrange expr :=
lazymatch goal with
| |- ?Rel ?L ?R =>
transitivity expr; [> ring | ]
| |- ?Other =>
fail "Goal is not a relation. Goal:" Other
end.
(* Finally, if we manipulate L using thatis or rewrite tactics, then we will
eventually get to the point where it matches R exactly, at which point one
normally uses the 'reflexivity' tactic, but that is confusing, if we had to
do manipulations precisely because our goal was not reflexive, and further,
reflexivity might sneak more beta/eta manipulations in, making our
annotations irrelevant. This tactic lets us check that L and R match
exactly, no beta/eta required, and only then uses reflexivity to finish the
proof. *)
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.
(* 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.
(* When manipulating dysjunctions, like the Even/Odd case coming up, it can
be useful to restate what our current hypotheses are. *)
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.
(* Now let's try and prove our theorem again, but using these self-documenting
tactics. *)
Theorem even_or_odd_chained: forall n: nat,
Even n \/ Odd n.
Proof.
induction n.
*havegoal (Even 0 \/ Odd 0).
left; havegoal (Even 0). (* Choose (Even 0) as a new goal. *)
exists 0; havegoal (2 * 0 = 0).
have (2 * 0).
thatis 0.
done.
*weknow IndCase: (Even n \/ Odd n).
destruct IndCase.
+havegoal (Even (S n) \/ Odd (S n)).
weknow IsEven: (Even n).
right; havegoal (Odd (S n)).
destruct IsEven as [k Eq].
weknow Eq: (2 * k = n).
exists k; havegoal (2 * k + 1 = S n).
apply eq_sym. (* Flip the goal, to work right to left. *)
have (S n).
rewrite <- Eq.
have (S (2 * k)).
rearrange (2 * k + 1).
done.
+havegoal (Even (S n) \/ Odd (S n)).
weknow IsOdd: (Odd n).
left; havegoal (Even (S n)).
destruct IsOdd as [k Eq].
exists (S k).
havegoal (2 * S k = S n).
apply eq_sym.
have (S n).
rewrite <- Eq.
have (S (2 * k + 1)).
rearrange (2 * S k).
done.
Qed.
(* That was much longer, but now the proof guides you through its own state,
and if you edit this file so that any of the annotations are wrong, the
proof no longer completes! In general our proofs are going to be pretty long
from now on, but hopefully they are much more pleasant to read.
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 'sequence' variation. *)
Definition sequence (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' => sequence (pascal r')
end.
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 (sequence (pascal r) 0).
thatis (pascal r 0).
want 1.
weknow Ind: (pascal r 0 = 1).
exact Ind.
Qed.
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.
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.
Lemma factorial_up: forall n: nat,
factorial n = prod_up n 1.
Proof.
intro n.
induction n.
*reflexivity.
*have (factorial (S n)).
thatis ((S n) * factorial n).
weknow Ind: (factorial n = prod_up n 1).
rewrite Ind.
have ((S n) * prod_up n 1).
rearrange ((n + 1) * prod_up n 1).
rewrite <- (prod_up_down n 1).
have (prod_up (1 + n) 1).
thatis (prod_up (S n) 1).
done.
Qed.
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.
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 (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.