15-317 Constructive Logic
Recitation 11: The Identity Theorem

Today in recitation, we went over selected solutions from Midterm 2, including the G4ip derivation of ¬¬(P ∨ ¬P) and the time-saving/wasting derivations. The take-home point from the problem about the memoizing rule is that remembering an atomic proposition can only save time, but remembering a proposition with structure opens us to the possibility of needlessly decomposing that proposition. We also went over the classical sequent calculus derivation of P ∨ ¬ P, the cut admissibility case, and the invertibility proof. See the complete solution for details.

Then, we went over two cases of the proof of the Identity Theorem for the intuitionistic sequent calculus, the case for atomic propositions and the case for conjunctions. The end of Lecture 9 details the proof for all connectives.

Finally, we saw how to represent the two cases of this proof in Twelf. To help convey the mindset of a Twelf programmer, we reproduce them here in a piecewise fashion. To see the steps animated, you may run animate.pl on identity-anim.txt.

First, we start from the specification of the theorem as a well-moded total logic program. The program relates each proposition A to a derivation that a hypothesis left A leads to a conclusion right A:

    identity : {A:prop} (left A -> right A) -> type.
    %mode identity +A -D.

    %worlds () (identity _ _).
    %total A (identity A _).
  

The first and easiest case is the case when the given proposition is an atomic proposition. We require a derivation of left (? P) -> right (? P), which we denote by leaving a "hole" _ with an appropriate type annotation:

    id?  : identity (? P) (_ : left (? P) -> right (? P)).
  
(NB: each intermediate step will typecheck in Twelf: by writing a type annotation, we can check that our intuitions are correct, but if we leave off the type annotation, Twelf will tell us what type it expects the hole to have. In this discussion, we will always write the type annotation for clarity.) The type we require is exactly the type of the init rule, so we can fill in the underscore using init:
    id?  : identity (? P) (init : left (? P) -> right (? P)).
  
If we run the above code through Twelf with the totality check, we see that although it typechecks, we get the following coverage error:
    identity.elf:7.8-7.9 Error:
    Coverage error --- missing cases:
    {A1:prop} {A2:prop} {X1:left (A1 => A2) -> right (A1 => A2)}
       |- identity (A1 => A2) ([x:left (A1 => A2)] X1 x),
    {A1:prop} {A2:prop} {X1:left (A1 /\ A2) -> right (A1 /\ A2)}
       |- identity (A1 /\ A2) ([x:left (A1 /\ A2)] X1 x).
  
This error expresses the fact that we are still missing clauses defining the identity relation when the first argument is either an implication or a conjunction.

Let's continue with the conjunction case. We can start off as before, by leaving a "hole" for the output and observing what its type is:

    id/\ : identity (A /\ B)
                    (_ : left (A /\ B) -> right (A /\ B)).
  
Since the output we require is of function type, and since we see no obvious ways of filling it in, we can start by expanding the hole to be an LF function. Its argument will have type left (A /\ B), and its body will have type right (A /\ B). We leave the body as a hole for now:
    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     _ : right (A /\ B)).
  

Now, we must find a way to make a right (A /\ B). Taking a cue from the paper proof, we know we'll want to start our derivation with the /\R rule:

    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (_ : right A)
                         (_ : right B)).
  
This leaves us with two holes: one of type right A and one of type right B.

Taking another cue from the paper proof, we can continue by noting that the first of these subderivations will proceed by the left rule \/L1, leaving two new holes:

    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 (_ : left A -> right A)
                               (_ : left (A /\ B)))
                         (_ : right B)).
  
How can we fill these in? Well, the left (A /\ B) that we expect to apply the rule to is exactly the one that we hypothesized to start our output derivation, namely u:
    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 (_ : left A -> right A)
                               (u : left (A /\ B)))
                         (_ : right B)).
  
What about the left A -> right A? We don't have one handy, but comparing with the paper proof, we realize that we should be able to come up with one using our induction hypothesis on the proposition A! In Twelf, appeals to the induction hypothesis correspond to recursive calls of the logic program, so we add a subgoal calling identity at the proposition A and naming the output IdA, with a type annotation on the output for readability.
    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 (_ : left A -> right A)
                               (u : left (A /\ B)))
                         (_ : right B))
            <- identity A (IdA : left A -> right A).
  
Now we can use the output IdA to satisfy our requirement of a term of type left A -> right A.
    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 (IdA : left A -> right A)
                               (u : left (A /\ B)))
                         (_ : right B))
            <- identity A (IdA : left A -> right A).
  

We can clean up a bit by deleting the extraneous type annotations from our application of /\L1:

    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 IdA u)
                         (_ : right B))
            <- identity A (IdA : left A -> right A).
  
And now, we can use symmetric reasoning to satisfy our final remaining hole of type right B:
    id/\ : identity (A /\ B)
                    ([u:left (A /\ B)]
                     /\R (/\L1 IdA u)
                         (/\L2 IdB u))
            <- identity A (IdA : left A -> right A)
            <- identity B (IdB : left B -> right B).
  
If we run this case through Twelf along with our previous case and the totality check, we get a new, smaller coverage error:
    identity.elf:14.8-14.9 Error:
    Coverage error --- missing cases:
    {A1:prop} {A2:prop} {X1:left (A1 => A2) -> right (A1 => A2)}
       |- identity (A1 => A2) ([x:left (A1 => A2)] X1 x).
  
Note that Twelf no longer complains about a missing case for conjunction! All that remains is the case for implication, and then the theorem will check successfully. The file identity.elf contains the entire proof, including the final case.

The two most important ideas to keep in mind while constructing a Twelf proof are the following:

  1. A proof is just a well-moded total logic program taking the inputs of the theorem to the outputs of the theorem.
  2. Let the types guide your development!

References:


[ Home | Schedule | Assignments | Handouts | Software ]

fp@cs
Frank Pfenning