How does the discriminate tactic work?
Asked Answered
O

2

8

I was curious about how the discriminate tactic works behind the curtain. Therefore I did some experiments.

First a simple Inductive definition:

Inductive AB:=A|B.

Then a simple lemma which can be proved by the discriminate tactic:

Lemma l1: A=B -> False.
intro.
discriminate.
Defined.

Let's see what the proof looks like:

Print l1.

l1 = 
fun H : A = B =>
(fun H0 : False => False_ind False H0)
  (eq_ind A
     (fun e : AB => match e with
                    | A => True
                    | B => False
                    end) I B H)
     : A = B -> False

This looks rather complicated and I do not understand what is happening here. Therefore I tried to prove the same lemma more explicitly:

Lemma l2: A=B -> False.
apply (fun e:(A=B) => match e with end).
Defined.

Let's again see what Coq has made with this:

Print l2.

l2 = 
fun e : A = B =>
match
  e as e0 in (_ = a)
  return
    (match a as x return (A = x -> Type) with
     | A => fun _ : A = A => IDProp
     | B => fun _ : A = B => False
     end e0)
with
| eq_refl => idProp
end
     : A = B -> False

Now I am totally confused. This is still more complicated. Can anyone explain what is going on here?

Onehorse answered 23/3, 2017 at 4:42 Comment(0)
G
11

Let's go over this l1 term and describe every part of it.

l1 : A = B -> False

l1 is an implication, hence by Curry-Howard correspondence it's an abstraction (function):

fun H : A = B =>

Now we need to construct the body of our abstraction, which must have type False. The discriminate tactic chooses to implement the body as an application f x, where f = fun H0 : False => False_ind False H0 and it's just a wrapper around the induction principle for False, which says that if you have a proof of False, you can get a proof of any proposition you want (False_ind : forall P : Prop, False -> P):

(fun H0 : False => False_ind False H0)
  (eq_ind A
     (fun e : AB => match e with
                    | A => True
                    | B => False
                    end) I B H)

If we perform one step of beta-reduction, we'll simplify the above into

False_ind False
          (eq_ind A
             (fun e : AB => match e with
                            | A => True
                            | B => False
                           end) I B H)

The first argument to False_ind is the type of the term we are building. If you were to prove A = B -> True, it would have been False_ind True (eq_ind A ...).

By the way, it's easy to see that we can simplify our body further - for False_ind to work it needs to be provided with a proof of False, but that's exactly what we are trying to construct here! Thus, we can get rid of False_ind completely, getting the following:

eq_ind A
  (fun e : AB => match e with
                 | A => True
                 | B => False
                 end) I B H

eq_ind is the induction principle for equality, saying that equals can be substituted for equals:

eq_ind : forall (A : Type) (x : A) (P : A -> Prop),
   P x -> forall y : A, x = y -> P y

In other words, if one has a proof of P x, then for all y equal to x, P y holds.

Now, let's create step-by-step a proof of False using eq_ind (in the end we should obtain the eq_ind A (fun e : AB ...) term).

We start, of course, with eq_ind, then we apply it to some x - let's use A for that purpose. Next, we need the predicate P. One important thing to keep in mind while writing P down is that we must be able to prove P x. This goal is easy to achieve - we are going to use the True proposition, which has a trivial proof. Another thing to remember is the proposition we are trying to prove (False) - we should be returning it if the input parameter is not A. With all the above the predicate almost writes itself:

fun x : AB => match x with
              | A => True
              | B => False
              end

We have the first two arguments for eq_ind and we need three more: the proof for the branch where x is A, which is the proof of True, i.e. I. Some y, which will lead us to the proposition we want to get proof of, i.e. B, and a proof that A = B, which is called H at the very beginning of this answer. Stacking these upon each other we get

eq_ind A
       (fun x : AB => match x with
                  | A => True
                  | B => False
                  end)
       I
       B
       H

And this is exactly what discriminate gave us (modulo some wrapping).

Geognosy answered 23/3, 2017 at 7:33 Comment(6)
Can one say that the last term above is the shortest way to make "discriminate" explicit ?Onehorse
@Onehorse I think we can. Alternatively, we can makes some reductions (using Eval cbv in fun H : A = B => [the above term].) and arrive at (fun H : A = B => match H in (_ = y) return match y with | A => True | B => False end with | eq_refl => I end).Geognosy
Is there a command for printing the discriminate tactic (or any other built-in tactic) ?Onehorse
@Onehorse What do you mean? Would you like to have access to its source code?Geognosy
I mean something like "Print Ltac"Onehorse
@Onehorse discriminate implemented in OCaml, not in Ltac. The standard library "imports" it in Coq.Init.Notations. Following it, we arrive at extratactics.ml4 and from there we get to equality.ml. You might want to start digging further from this point.Geognosy
N
8

Another answer focuses on the discriminate part, I will focus on the manual proof. You tried:

Lemma l2: A=B -> False.
apply (fun e:(A=B) => match e with end).
Defined.

What should be noted and makes me often uncomfortable using Coq is that Coq accepts ill-defined definitions that it internally rewrites into well-typed terms. This allows to be less verbose, since Coq adds itself some parts. But on the other hand, Coq manipulates a different term than the one we entered.

This is the case for your proof. Naturally, the pattern-matching on e should involve the constructor eq_refl which is the single constructor of the eq type. Here, Coq detects that the equality is not inhabited and thus understands how to modify your code, but what you entered is not a proper pattern-matching.

Two ingredients can help understand what is going on here:

  • the definition of eq
  • the full pattern-matching syntax, with as, in and return terms

First, we can look at the definition of eq.

Inductive eq {A : Type} (x : A) : A -> Prop :=  eq_refl : x = x.

Note that this definition is different from the one that seems more natural (in any case, more symmetric).

Inductive eq {A : Type} : A -> A -> Prop :=  eq_refl : forall (x:A), x = x.

This is really important that eq is defined with the first definition and not the second. In particular, for our problem, what is important is that, in x = y, x is a parameter while y is an index. That is to say, x is constant across all the constructors while y can be different in each constructor. You have the same difference with the type Vector.t. The type of the elements of a vector will not change if you add an element, that's why it is implemented as a parameter. Its size, however, can change, that's why it is implemented as an index.

Now, let us look at the extended pattern-matching syntax. I give here a very brief explanation of what I have understood. Do not hesitate to look at the reference manual for safer information. The return clause can help specify a return type that will be different for each branch. That clause can use the variables defined in the as and in clauses of the pattern-matching, which binds respectively the matched term and the type indices. The return clause will both be interpreted in the context of each branch, substituting the variables of as and in using this context, to type-check the branches one by one, and be used to type the match from an external point of view.

Here is a contrived example with an as clause:

Definition test n :=
  match n as n0 return (match n0 with | 0 => nat | S _ => bool end) with
  | 0 => 17
  | _ => true
  end.

Depending on the value of n, we are not returning the same type. The type of test is forall n : nat, match n with | 0 => nat | S _ => bool end. But when Coq can decide in which case of the match we are, it can simplify the type. For example:

Definition test2 n : bool := test (S n).

Here, Coq knows that, whatever is n, S n given to test will result as something of type bool.

For equality, we can do something similar, this time using the in clause.

Definition test3 (e:A=B) : False :=
  match e in (_ = c) return (match c with | B => False | _ => True end) with
  | eq_refl => I
  end.

What's going on here ? Essentially, Coq type-checks separately the branches of the match and the match itself. In the only branch eq_refl, c is equal to A (because of the definition of eq_refl which instantiates the index with the same value as the parameter), therefore we claimed we returned some value of type True, here I. But when seen from an external point of view, c is equal to B (because e is of type A=B), and this time the return clause claims that the match returns some value of type False. We use here the capability of Coq to simplify pattern-matching in types that we have just seen with test2. Note that we used True in the other cases than B, but we don't need True in particular. We only need some inhabited type, such that we can return something in the eq_refl branch.

Going back to the strange term produced by Coq, the method used by Coq does something similar, but on this example, certainly more complicated. In particular, Coq often uses types IDProp inhabited by idProp when it needs useless types and terms. They correspond to True and I used just above.

Finally, I give the link of a discussion on coq-club that really helped me understand how extended pattern-matching is typed in Coq.

Numismatics answered 23/3, 2017 at 13:39 Comment(1)
This is (imho) a nice answer, which explains the difference between parameters and indices.Geognosy

© 2022 - 2024 — McMap. All rights reserved.