Computer Program as Proof

“All your proof belong to us”

The first way we learn to do proofs is by induction. Proofs by induction are done in three steps. First, we establish a base case. Next we assume a hypothesis, and finally, we prove the inductive step. As an example let us consider the fact that the sum $0+1+2+ \ldots+n$ is $n(n+1)/2$. The three steps are:

  • Base case: consider the series $0+1$, the sum is $1$, and the formula is $1(2)/2$. So, the formula and the actual series sum agree.

  • Induction hypothesis: let’s assume that $0+1+\ldots + (k-1)$ is $k(k-1)/2.$

  • Inductive step: we want to show that the sum $0+1+2+ \ldots + k$ is $k(k+1)/2$. By our hypothesis the series sum is $k(k-1)/2 + k = k(k+1)/2$.

The proof above is clear enough for us. The question is whether the proof above is machine readable and verifiable. In the ordinary course, we write programs and argue for the correctness of these programs. What if, we were able to write a program which could be verified by another program? Interestingly enough programs can be written to provide proofs, as is illustrated next.

Here is a short program in a functional programming language coq to compute the sum of the first $n$ integers.

Require Import Arith.

Fixpoint sum (n:nat) : nat :=
  match n with
  | 0 => 0
  | S m => n + sum m

Compute (sum 10).

nat is a type which represents natural numbers. The natural numbers are 0, S 0, S S 0, ... where S is the type constructor. The code is recursive, and the base case along with the recursive step is specified using the construct match (for template matching). The last line Compute (sum 10) computes the sum $0+1+\ldots +10$.

Typically, we would prove that the program above is correct by writing an induction proof as the one listed above. We can also specify the proof in specification language such as ‘Gallina.’ A Gentzen style proof system can be used to verify each step. We specify the proof as a sequence of steps. Every step follows from the previous step given the rules in the proof system. Machine verification of inductive proofs is interesting. Not only we can check for bugs, they can also be graded automatically.

The method is illustrated next. Think of Gallina as specifying a proof, rather than discovering a proof using an automated process. If the coq compiler accepts the specification, then the proof is valid otherwise it is not. The proof discovery process cannot usually be automated because of undecidability issues.

Let us start with a few basic facts about the type nat. We will use these Lemmas later in the final specification.

Let us first prove that sum (S n) = (S n) + (sum n). The specification of the proof of this fact in Gallina is below.

Lemma l1: forall n:nat, sum (S n) = (S n) + (sum n).

   induction n as [|m IH]. 

   - (*2*0 = 0 *) simpl. reflexivity.
   - (* 2*sum(n) = 2n + sum(n-1) *) rewrite -> IH. simpl. reflexivity.


The lemma itself is a statement in predicate logic. The proof starts with Proof and ends with Qed. This is an inductive proof on n, as specified by the keyword induction. The as clauses identifies the two subgoals that need to be proven. The goals are within [] and separated by |. In the first empty goal to the left of |, n is replaced with 0. In the second subgoal called IH, nis replaced withS m.` The two subgoals are proved in the two bullets. Simplification and the reflexivity establish the base case. Now, let us examine the second subgoal. The goal is below the horizontal line, and the assumptions are above the horizontal line.

1 subgoals
m : nat
IH : sum (S m) = S m + sum m
sum (S (S m)) = S (S m) + sum (S m)

Rewriting the subgoal using the induction hypothesis gives us.

sum (S (S m)) = S (S m) + (S m + sum m)

Simplification gives us

S (S (m + S (m + sum m))) = S (S (m + S (m + sum m)))

The similarity of the two terms can be verified by reflexivity.

Furthermore, let us prove that S (S n) = S (n+1).

Lemma l2: forall n:nat, S (S n) = S (n+1).
  induction n; intros. simpl.

  - simpl.
  - rewrite  <- IHn. reflexivity.

Once again the proof is by induction on n. intros adds the variables that are quantified by the for all quantifier to the context. Use of intros is redundant here. simpl states that each subgoal should be simplified at the start. The two bullets are the two subcases.

2 subgoal
2 = 2
S (S (S n)) = S (S n + 1)

The first subgoal is readily proved using reflexivity. The system assumes the IH to prove the second subgoal. The rewriting of the subgoal as the commands are processed is shown below, until the LHS and the RHS are the same.

1 subgoals
n : nat
IHn : S (S n) = S (n + 1)
S (S (S n)) = S (S n + 1)                    simplify.
S (S (S n)) = S (S (n + 1))                  rewrite <- IH.     
S (S (S n)) = S (S (S n))                    reflexivity.

We also need the following Lemma.

Lemma l3: forall n:nat, n+1 = S n.
  induction n; intros. simpl.

  rewrite  <- IHn.

Finally, the proof

Theorem gauss_sum: forall n:nat, 2*(sum n) = n * (S n).

   induction n as [|m IH]; intros. simpl.

   - (*2*0 = 0 *) simpl. reflexivity.
   - (* 2*sum(n) = 2n + sum(n-1) *)  
     rewrite -> l1. 

     rewrite mult_plus_distr_l.
     rewrite -> IH.

     replace (S m) with (m+1).
     replace (S (m+1)) with (2+m).
     rewrite <- mult_plus_distr_r.
     rewrite mult_comm.


     rewrite  l2.

     rewrite l3.

The first subgoal (the base case) can be proved easily using induction. For the second subgoal (the induction step), we have to massage the terms a lot to get them in the right shape, before reflexivity can be used. The output below shows the terms in the first column, the operation listed is in the second column.

Subgoals operation
2 * sum (S m) = S m * S (S m) rewrite -> l1.
r2 * (S m + sum m) = S m * S (S m) rewrite mult_plus_distr_l.
2 * S m + 2 * sum m = S m * S (S m) rewrite -> IH.
2 * S m + m * S m = S m * S (S m) replace (S m) with (m+1).

The replace operations creates two new subgoals: in additional to the original term, we have to also prove that (S m) = (m+1) and (S (m+1)) = (2+m).

3 subgoal
m : nat
IH : 2 * sum m = m * S m
2 * (m + 1) + m * (m + 1) = (m + 1) * (2 + m)
2 + m = S (m + 1)
m + 1 = S m

The original subgoal is proved as

(2 + m) * (m + 1) = (m + 1) * (2 + m)
(m + 1) * (2 + m) = (m + 1) * (2 + m)

The next two subgoals are proved by applications of Lemma l2 and l3 respectively.

S (S m) = S (m + 1)
S (m + 1) = S (m + 1)

and as

m + 1 = S m
S m = S m

The proof tree is shown below, and the coq code is available here. The nodes in the proof tree list the subgoals and the assumptions used to prove the subgoals. The labels on the edges correspond to the operations in the code.

Further reading: See Proofs are Programs by Walder and learn more about Curry-Howard isomorphism.