On this page:
10.1 Some Facts About Natural Numbers
10.2 An inductive proof is a recursive function over derivations
7.5.0.17

10 Lecture 5 and 6 – Proof by Induction

In Lecture 4 – Type Systems we learend about type systems. The point of a type system is to make predictions. But we don’t just want to make predictions about a single term, like z. We want to make predictions about whole classes of terms, like all terms of type Nat, or any well-typed term.

A prediction is a theorem, so we need to write a proof to be sure our prediction is true. So far, we’ve seen one proof technique: build a derivation. This essentially corresponds proof by Implication. Judgments express implication: if A then B. To show B, I build a derivation B, by build a derivation of A, and appling some rule.

Unfortunately, we can only build derivations when we have concrete, particular terms. Rules refer to particular terms, like z, or true, or (s e). How do we prove a theorem about an arbitrary term e? There is no known structure to e, so we can’t immediately build a derivation.

For this, we need Induction, our second axiom:

If I have an inductively defined judgment, J, with rules R0, R1, ..., RN, and I want prove some property P holds for all derivations of J, then it suffices to prove:
  • If P holds on the recursive subderivations of R0, then P holds for derivations beginning with R0

  • If P holds on the recursive subderivations of R1, then P holds for derivations beginning with R1

  • ...

  • If P holds on the recursive subderivations of RN, then P holds for derivations beginning with RN

This is an axiom, not a provable fact. This is an assertion by mathematicians that if we do this, then we use this proof technique, then we can believe the proof. We can’t prove this axiom is reasonable; we trust that it is reasonable. It’s counter-intuitive, since it allows us to assume the very thing we’re trying to prove (albeit, assume it in a "smaller" way). But so far, no proofs by induction have been at the root of an proof of an untrue theorem, so it seems okay to use.

A good rule of thumb is that any time we have a theorem with some meta-varaible with no additional structure, like e, we need induction. This isn’t always true. We may be able to appeal to previously proved lemmas that will let us build generic derivations. Sometimes, our judgments will happen to let us build derivations even when we have no structure. But it’s a good rule of thumb if you’re not sure where to start.

10.1 Some Facts About Natural Numbers

Let’s warm up to induction by proving some things about natural numbers. Natural numbers are an inductively defined judgment, given below (as a syntax):

n, m ::= z | (s n) | n + m

Or below, as a judgment:

[ z : Nat ]

 

------- [Z-Nat]

z : Nat

 

 

n : Nat

--------- [S-Nat]

s n : Nat

 

 

n : Nat

m : Nat

----------- [S-Plus]

n + m : Nat

We also define a simple reduction judgment and evaluation judgment:

[ m -> m ]

 

---------- [Step-Plus-Z]

z + m -> m

 

 

---------------------- [Step-Plus-S]

(s n) + m -> n + (s m)

[ eval(m) = n ]

 

 m ->* n

----------

eval(m) = n

I leave the conversion judgment undefined, but it’s the obvious reflexive, transitive, compatible closure of the reduction judgment.

We’ll start simple:

Theorem: (For all m,) eval(0 + m) = m

This theorem says that evaluating 0 plus any m equals m. Often, we would leave out of the explicit quantification over all possible m in the theorem statement. I’ve written it explicitly in parentheses to suggest this quantification is "obvious" (to a working PL theorist) from context.

The theorem is probably intuitively true to anyone reading it. And of course, it is true. But true is not the same as being proven, so let’s see if we can prove it.

Theorem: (For all m,) eval(0 + m) = m

Proof:

Trivial, by the following derivation

 ------------ [Step-Plus-Z]

 (0 + m) -> m

 ------------ [Step]

 0 + m ->* m

---------------

eval(0 + m) = m

Huh. That proof worked without any kind of induction even those it had a for all quantification.

But this was an accident of our reduction judgment. It just so happens that the [Step-Plus-Z] rule works for any m, as long as the left hand side is 0. If we try to do the proof that eval(m + 0) = m, it would be so easy:

Theorem: (For all m,) eval(m + 0) = m

Proof:

... erm, there is no reduction rule to apply, so we can’t (immediately) build a derivation.

The proof proceeds by induction on the structure of m (by induction on the derivation that m : Nat).
  • Case: Suppose that m : Nat is the derivation

    ------- [Z-Nat]

    z : Nat

    We must show that eval(z + z) = z. This is trivial, since z + z -> z.

  • Case: Suppose that m : Nat is the derivation

    n : Nat

    ------- [S-Nat]

    s n : Nat

    We must show that eval((s n) + 0) = s n.

    By the induction hypothesis applied to the sub-derivation n : Nat that eval(n + 0) = n.

    Note that we can easily show that (s n) + 0 -> n + (s 0) But we can’t seem to combine this with anything. By deconstructing the derivation we get from the induction hypothesis, we know

    n + 0 ->* n

    ---------------

    eval(n + 0) = n

    But it’s not clear how to combine that fact with something of the shape (s n) + 0 ->* n + (s 0) to build any derivation. So how do we proceed?

In general, when stuck in a proof, we have two options: change our theorem, or change our model. We could try to separate that n + (s 0) ->* s n, and that might work. It certainly ought to be true. But actually, we can more easily complete the proof by changing our reduction rule to something equivalent, but simpler to prove things about.

This is a pretty common trick in programming languages work. If you see a judgment that seems correct, but different than the obvious thing, you should ask yourself if this judgment is easier to use in proofs.

We change the reduction judgment to the following:

[ m -> m ]

 

---------- [Step-Plus-Z]

z + m -> m

 

 

---------------------- [Step-Plus-S]

(s n) + m -> s (n + m)

Note that now addition reduces to an addition where both sub-expressions are inductively smaller. This is a good sign that it will work better in inductive proofs.

Theorem: (For all m,) eval(m + 0) = m

Proof:

... erm, there is no reduction rule to apply, so we can’t (immediately) build a derivation.

The proof proceeds by induction on the structure of m (by induction on the derivation that m : Nat).

  • Case: Suppose that m : Nat is the derivation

    n : Nat

    ------- [S-Nat]

    s n : Nat

    We must show that eval((s n) + 0) = s n.

    By the induction hypothesis applied to the sub-derivation n : Nat that eval(n + 0) = n. Recall that we are allowed to assume the very theorem we’re trying to prove, but only for sub-derivations of the thing we’re doing induction on. Here, we’re doing induction on m : Nat, and we are in the case where m is s n, so we are allowed to assume the theorem holds for n : Nat. This is the tricky part of an inductive proof.

    Note that we can easily show that (s n) + 0 -> s (n + 0).

    Note we can now easily combine this with the fact we get from the induction hypothesis.

    We know that n + 0 ->* n, since

    n + 0 ->* n

    ---------------

    eval(n + 0) = n

    Recall the compatibility rule for s:

      n ->* n'

    ----------- [S-Compat]

    s n ->* s n'

    So we know that s (n + 0) ->* s n. We have now shown:

        (s n) + 0

    ->* s (n + 0)

    ->* s n

    Which means eval((s n) + 0) = s n.

10.2 An inductive proof is a recursive function over derivations

It’s sometimes mysterious to see an on-paper proof. What tells us that the series of words I just wrote constitutes a proof? What is a proof? How do I understand a proof that assumes the very thing it is proving? What is an induction hypothesis?

These are all questions I’ve had too. I didn’t really understand proofs until I understood that proofs are programs. A theorem is a function: is has inputs (its premises) and produces outputs (its conclusion). An inductive proof is a recursive function: it pattern matches on its input, deconstructing a derivation, builds a new derivation, and occassionally recurs on a sub-derivation.

Understanding that proofs are programs is useful sometimes. It’s actually useful, sometimes, to structure a proof as loop with an accumulator. I’ve done this in one paper. It’s hard to think about that until you understand proofs as programs.

We don’t need any fancy proof assistant to see this. We can write recursive functions in any language. I’ll choose Racket since Redex provides derivations that Racket understands.

Below, I give a model of the natural numbers in Redex. I use the judgment style for defining them. I redefine n and m to be aliases for any, to make the judgments more readable.

Examples:
> (define-language U
    [n m ::= any])
> (define-judgment-form U
    #:contract ( n : Nat)
  
    [----------- "Z"
     ( z : Nat)]
  
    [( n : Nat)
     --------------- "S"
     ( (s n) : Nat)]
  
    [( any_1 : Nat)
     ( any_2 : Nat)
     ----------------------- "Add"
     ( (any_1 + any_2) : Nat)])
> (define-judgment-form U
    #:contract (-> n n)
  
    [--------------- "Step-Add-Z"
     (-> (z + n) n)]
  
    [-------------------------- "Step-Add-S"
     (-> ((s n) + m) (s (n + m)))])
> (define-judgment-form U
    #:contract (->* n n)
  
    [---------- "Refl"
     (->* n n)]
  
    [(-> n_1 n_2)
     ---------- "Step"
     (->* n_1 n_2)]
  
    [(->* n_1 n_2)
     (->* n_2 n_3)
     ---------- "Trans"
     (->* n_1 n_3)]
  
    [(->* n_1 n_11)
     (->* n_2 n_21)
     --------------- "Add-Compat"
     (->* (n_1 + n_2) (n_11 + n_21))]
  
    [(->* n_1 n_2)
     --------------- "S-Compat"
     (->* (s n_1) (s n_2))])
> (define-judgment-form U
    [(->* n m)
     -----------
     (eval n m)])

We’ve seen how to do simple proofs in Redex: building a derivation is a proof. (Note Redex normally won’t accept a derivation with a meta-variable in it; this only works because the symbol n matches any. I’m relying on a pun in my code and this is not always wise.)

Example:
> (test-judgment-holds
   eval
   (derivation
    `(eval (z + n) n)
    #f
    (list
     (derivation
      `(->* (z + n) n)
      "Step"
      (list
       (derivation `(-> (z + n) n) "Step-Add-Z" (list)))))))

But we can also do inductive proofs. This requires us to understand a little bit about Racket: we need to know how to pattern match on derivations, which involves using the pattern language. (Note that Racket allows unusual characters in names, so we can use function names like "n+0-equals-n".)

Examples:
> (require racket/match)
; Derivation (⊢ n : Nat) -> Derivation (eval (n + 0) n)
; Requires a derivation that n is a natural number, returns a derivation that n + 0 evaluates to n.
> (define (n+0-equals-n_incomplete d)
    ; Pattern match on the derivation d
    (match d
      ; Case: the derivation begins:
      ; 
      ; --------- "Z"
      ; ⊢ z : Nat
      [(derivation `( z : Nat) "Z" (list))
       ; We must show that eval (0 + 0) = 0
       ; Construct the derivation as follows:
       (derivation
        `(eval (z + z) z)
        #f
        (list
         (derivation
          `(->* (z + z) z)
          "Step"
          (list
           (derivation `(-> (z + z) z) "Step-Add-Z" (list))))))]
      [_ (error "Incomplete proof")]))

So far, the proof is incomplete. That’s okay; we can run it as long as we don’t reach the incomplete case:

Example:
> (n+0-equals-n_incomplete
   (derivation `( z : Nat) "Z" (list)))

(derivation

 '(eval (z + z) z)

 #f

 (list

  (derivation

   '(->* (z + z) z)

   "Step"

   (list (derivation '(-> (z + z) z) "Step-Add-Z" '())))))

The proof transforms one derivation, that z is a natural number, into another, that eval (z + z) = z.

Examples:
; Derivation (⊢ n : Nat) -> Derivation (eval (n + 0) n)
; Requires a derivation that n is a natural number, returns a derivation that n + 0 evaluates to n.
> (define (n+0-equals-n d)
    ; Pattern match on the derivation d
    (match d
      [(derivation `( z : Nat) "Z" (list))
       ; In the z case, reuse our earlier proof that this works for z
       (n+0-equals-n_incomplete d)]
      [(derivation `( (s ,n) : Nat) "S" (list sub-derivation))
       ; Case: the derivation begins:
       ;  ⊢ n : Nat
       ;  --------- "S"
       ;  ⊢ (s n) : Nat
       (derivation
        `(eval ((s ,n) + z) (s ,n))
        #f
        (list
         (derivation
          `(->* ((s ,n) + z) (s ,n))
          "Trans"
          (list
           (derivation
            `(->* ((s ,n) + z) (s (,n + z)))
            "Step"
            (list
             (derivation `(-> ((s ,n) + z) (s (,n + z))) "Step-Add-S" (list))))
           (derivation
            `(->* (s (,n + z)) (s ,n))
            "S-Compat"
            ; Requires a proof that (n + z) ->* n
            ; This follows by the induction hypothesis applied to n, since (eval
            ; (n + z) = n) implies (n + z) ->* n
            (list
             ; Appealing to the induction hypothesis is recursion.
             ; We then destruct the derivation we get from recursion.
             (match (n+0-equals-n sub-derivation)
               [(derivation `(eval (,n + z) ,n) #f (list d2))
                d2])))))))]
      [(derivation `( (,n + ,m) : Nat) "Plus" (list sub-derivation-n sub-derivation-m))
       ; Case: the derivation begins:
       ;  ⊢ n : Nat
       ;  ⊢ m : Nat
       ;  --------- "Plus"
       ;  ⊢ n + m : Nat
       (error "This theorem isn't true so we can't complete this case.")]))

In the second case of this proof, we know that the list of premises contains one sub-derivation, which we name so we can use it later. We use the "unquote" pattern, ,n, to bind whatever expression the s is applied to. This is part of Racket’s pattern language; we know there is some term there, but we don’t know what. Since it’s a quasiquoted list, we need to use comma to tell Racket to treat that as a pattern variable instead of a symbol.

Then we proceed to build a derivation that eval ((s n) + z) = (s n). Most of this is standard, building derivations by hand, appealing to rules, and building derivations for premises.

But, eventually we get to a point where we can’t apply any rule directly. We need to prove that (n + z) ->* n, and there is no rule we can apply, since we don’t know anything about n. Thankfully, we can gleam this fact from induction! We recursively call the function on the sub-derivation that n is a natural number. We get back a derivation about eval, so we destruct it. We could have written the code more cleanly by using a helper function, i.e., a lemma.

Examples:
> (n+0-equals-n
   (derivation `( z : Nat) "Z" (list)))

(derivation

 '(eval (z + z) z)

 #f

 (list

  (derivation

   '(->* (z + z) z)

   "Step"

   (list (derivation '(-> (z + z) z) "Step-Add-Z" '())))))

> (n+0-equals-n
   (derivation
    `( (s z) : Nat)
    "S"
    (list
     (derivation `( z : Nat) "Z" (list)))))

(derivation

 '(eval ((s z) + z) (s z))

 #f

 (list

  (derivation

   '(->* ((s z) + z) (s z))

   "Trans"

   (list

    (derivation

     '(->* ((s z) + z) (s (z + z)))

     "Step"

     (list (derivation '(-> ((s z) + z) (s (z + z))) "Step-Add-S" '())))

    (derivation

     '(->* (s (z + z)) (s z))

     "S-Compat"

     (list

      (derivation

       '(->* (z + z) z)

       "Step"

       (list (derivation '(-> (z + z) z) "Step-Add-Z" '())))))))))

Unfortunately, when we try to finish the proof case for n + m, we discover that our theorem isn’t general enough. I leave it as an exercise to the reader to discover the fix. (Hint: evaluation and equality are different, but similar, judgments.)

Formally, a proof can’t just be any function, though. It must be a complete, bug-free function. It must always return something of the right type, handle all cases of any data it consumes, and always terminate. It’s hard to trust a proof written in Racket since Racket can’t check any of these things, but it’s easier to get started writing such a proof. This is why many proof assistants aren’t Turing-complete; it’s very useful to avoid the halting problem when one wants to check that a function terminates by making it impossible to write an infinite loop.

But, thinking of proofs as functions can be helpful.