摘录自《Software Foundation》,旨在交流与学习Coq的基本用法; 强烈建议大家下载源码自己运行一遍
前面已经通过simplification证明了0 + n = n
,现在尝试证明n = n + 0
Theorem plus_n_O_firsttry : forall n:nat,
n = n + 0.
Proof.
intros n.
simpl. (* Does nothing! *)
Abort.
可以看到,reflexivity
在这里无能为力,因为n
和n + 0
都是未知数,所以plus
函数的match
无法匹配。如果用destruct
呢?
Theorem plus_n_O_secondtry : forall n:nat,
n = n + 0.
Proof.
intros n. destruct n as [| n'].
- (* n = 0 *)
reflexivity. (* so far so good... *)
- (* n = S n' *)
simpl. (* ...but here we are stuck again *)
Abort.
运行以上代码会发现,destruct
将n
分为O
(也就是0)与S n'
(也就是非0)两种情况。n
为0时可以证明,但n = S n'
时仍然证明不了——即使将n'
再destruct
。
回忆数学归纳法的使用。Coq中也提供了类似的证明方法。
注意这里的
induction
不是定义类型的inducive
Coq中提供了一个叫induction
的tactic,它把上述定理的证明分成两个子目标:
P(O)
成立P(n')
成立可以推导出P(S n')
下面是证明过程
Theorem plus_n_O : forall n:nat, n = n + 0.
Proof.
intros n. induction n as [| n' IHn'].
- (* n = 0 *) reflexivity.
- (* n = S n' *) simpl. rewrite <- IHn'. reflexivity. Qed.
与destruct
类似,用as
来设定两个子目标(用|
分开)。
n
被替换成O
,所以不需要引入新的变量n
被替换成S n'
,引入了新的变量n'
和名为IHn'
的归纳假设n' = n' + O
,新的变量的名字和归纳假设的名字都在as
的第二部分中指出。第一个子目标通过简化可以解决。
第二个子目标为S n' = (S n') + 0
,通过简化变为S n' = S (n' + 0)]
,再通过对归纳假设进行从右到左的替换就可以证明。
下面是induction
的另一个实例
Theorem minus_diag : forall n,
minus n n = 0.
Proof.
(* WORKED IN CLASS *)
intros n. induction n as [| n' IHn'].
- (* n = 0 *)
simpl. reflexivity.
- (* n = S n' *)
simpl. rewrite -> IHn'. reflexivity. Qed.
上面证明minus_diag
定理的例子中intros
的使用是多余的,因为induction
这个关键词在碰到量词修饰的变量(比如上面的n
时)会自动把这些变量加入到当前的上下文中。
与非形式化的数学一样,在Coq中,大型的证明过程会分成很多小步骤,并往往会有很多引理。但这些引理有时候我们不需要给它起一个名字——一是不然定理的名字就太多了,二是会干扰到其它定理的证明。这时我们可以在碰到需要用到这个引理的时候再当场证明它就好了。这就是Coq中的一个tactic叫做assert
比如说,证明mult_0_plus
的时候用到了plus_0_n
,我们可以直接在证明中定义并引用局部引理。
Theorem mult_0_plus' : forall n m : nat,
(0 + n) * m = n * m.
Proof.
intros n m.
assert (H: 0 + n = n). { reflexivity. }
rewrite -> H.
reflexivity. Qed.
这里将提出来的引理起名为H
,并在后面的{}
中予以证明。
下面是另一个例子。
Theorem plus_rearrange_firsttry : forall n m p q : nat,
(n + m) + (p + q) = (m + n) + (p + q).
Proof.
intros n m p q.
(* We just need to swap (n + m) for (m + n)... seems
like plus_comm should do the trick! *)
rewrite -> plus_comm.
(* Doesn't work...Coq rewrote the wrong plus! *)
Abort.
这个例子中使用到了rewrite
,但因为rewrite
不是非常聪明,plus_comm
定理证明的n + m = m + n
只能用到最外层的+
上,而括号里面的+
没有实现重写。所以这里只用plus_comm
是证明不了的。
因此这里可以使用一个“局部”(local)的引理来证明n + m = m + n
,其中的n
和m
是就是mult_0_plus
中的n
和m
。
这里局部引理的另一个好处可以看出来了:它可以直接作用于证明过程中的“局部变量”
回忆用inductive
定义的类型,它们的每个构造器可以接收任何数量的参数,比如true
和O
接收0个参数,S
接收1个参数。下面定义的natprod
类型的构造器pair
则接收2个参数:
Inductive natprod : Type :=
| pair : nat -> nat -> natprod.
这里的pair
其实已经在Coq标准库中定义。输入Check pair
可知:
pair
: ?A -> ?B -> ?A * ?B
where
?A : [ |- Type]
?B : [ |- Type]
这个类型的声明可以这样理解:
“There is just one way to construct a pair of numbers: by applying the constructor [pair] to two arguments of type [nat].”
下面两个函数用于提取pair
的第1部分以及第2部分:
Definition fst (p : natprod) : nat :=
match p with
| pair x y => x
end.
Definition snd (p : natprod) : nat :=
match p with
| pair x y => y
end.
然后为pair
定义符号:
Notation "( x , y )" := (pair x y).
下面我们尝试定义类型natlist
——“自然数队列”。首先要明确队列是什么样的:
“A list is either the empty list or else a pair of a number and another list.”
定义如下:
Inductive natlist : Type :=
| nil : natlist
| cons : nat -> natlist -> natlist.
比如下面含有3个元素的队列:
Definition mylist := cons 1 (cons 2 (cons 3 nil)).
一直写cons
构造器比较难看,所以引入符号::
Notation "x :: l" := (cons x l)
(at level 60, right associativity).
Tips: 这里要记得
::
前面是一个元素,后面是一个列表
为了方便列表的构建,还可以使用[]
来作为notation:
Notation "[ ]" := nil.
Notation "[ x ; .. ; y ]" := (cons x .. (cons y nil) ..).
这里有一些细节先忽略
这里是类型bag
几个函数的定义。与前面有所不同的是,这些函数大多数是直接使用已有的函数:
Definition bag := natlist.
Definition sum : bag -> bag -> bag := app.
Definition add (v:nat) (s:bag) : bag := (v :: s).
Definition member (v:nat) (s:bag) : bool := negb (beq_nat O (count v s)).
其中sum
是两个bag
的内容相加,add
是一个数加到一个bag
里面,member
是判断数v
是否在s
里面。
Fixpoint nth_error' (l:natlist) (n:nat) : natoption :=
match l with
| nil => None
| a :: l' => if beq_nat n O then Some a
else nth_error' l' (pred n)
end.
上面的代码等价于
Fixpoint nth_error (l:natlist) (n:nat) : natoption :=
match l with
| nil => None
| a :: l' => match beq_nat n O with
| true => Some a
| false => nth_error l' (pred n)
end
end.
因为Coq并没有内置的布尔类型,所以Coq支持任何有两个构造器的类型作为条件表达式,如果计算结果是第一个构造器则条件表达式的结果为真,否则条件表达式的结果为假。