How do we know all Coq constructors are injective and disjoint?

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-05 22:16:54

When you define an inductive data type in Coq, you are essentially defining a tree type. Each constructor gives a kind of node that is allowed to occur in your tree, and its arguments determine the children and elements that that node can have. Finally, functions defined on inductive types (with the match clause) can check the constructors that were used to produce a value of that type in arbitrary ways. This makes Coq constructors very different from constructors you see in an OO language, for instance. An object constructor is implemented as a regular function that initializes a value of a given type; Coq constructors, on the other hand, are enumerating the possible values that the representation of our type allows. To understand this difference better, we can compare the different functions we can define on an object in a traditional OO language, and on an element of an inductive type in Coq. Let's use your num type as an example. Here's an object-oriented definition:

class Num {
    int val;

    private Num(int v) {
        this.val = v;
    }

    /* These are the three "constructors", even though they
       wouldn't correspond to what is called a "constructor" in
       Java, for instance */

    public static zero() {
        return new Num(0);
    }

    public static succ(Num n) {
        return new Num(n.val + 1);
    }

    public static doub(Num n) {
        return new Num(2 * n.val);
    }
}

And here's a definition in Coq:

Inductive num : Type :=
| zero : num
| succ : num -> num
| doub : num -> num.

In the OO example, when we write a function that takes a Num argument, there's no way of knowing which "constructor" was used to produce that value, because this information is not stored in the val field. In particular Num.doub(Num.succ(Num.zero())) and Num.succ(Num.succ(Num.zero())) would be equal values.

In the Coq example, on the other hand, things change, because we can determine which constructor was used to form a num value, thanks to the match statement. For instance, using Coq strings, we could write a function like this:

Require Import Coq.Strings.String.

Open Scope string_scope.

Definition cons_name (n : num) : string :=
  match n with
  | zero   => "zero"
  | succ _ => "succ"
  | doub _ => "doub"
  end.

In particular, even though your intended meaning for the constructors implies that succ (succ zero) and doub (succ zero) should be "morally" equal, we can distinguish them by applying the cons_name function to them:

Compute cons_name (doub (succ zero)). (* ==> "doub" *)
Compute cons_name (succ (succ zero)). (* ==> "succ" *)

As a matter of fact, we can use match to distinguish between succ and doub in arbitrary ways:

match n with
| zero   => false
| succ _ => false
| doub _ => true
end

Now, a = b in Coq means that there is no possible way we can distinguish between a and b. The above examples show why doub (succ zero) and succ (succ zero) cannot be equal, because we can write functions that don't respect the meaning that we had in mind when we wrote that type.

This explains why constructors are disjoint. That they are injective is actually also a consequence of pattern-matching. For instance, suppose that we wanted to prove the following statement:

forall n m, succ n = succ m -> n = m

We can begin the proof with

intros n m H.

Leading us to

n, m : num
H : succ n = succ m
===============================
n = m

Notice that this goal is by simplification equivalent to

n, m : num
H : succ n = succ m
===============================
match succ n with
| succ n' => n' = m
| _       => True
end

If we do rewrite H, we obtain

n, m : num
H : succ n = succ m
===============================
match succ m with
| succ n' => n' = m
| _       => True
end

which simplifies to

n, m : num
H : succ n = succ m
===============================
m = m

At this point, we can conclude with reflexivity. This technique is quite general, and is actually at the core of what inversion does.

There is none: the constructors O, S and D are indeed disjoint and injective but the semantics for nums you have in your head is not, as a function, injective.

That is why num would usually be considered to be a bad representation of the natural numbers: working up-to equivalence is quite annoying.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!