摘录自《Software Foundation》,旨在交流与学习Coq的基本用法; 强烈建议大家下载源码自己运行一遍
定义一个类型(type):
Inductive day : Type :=
| monday : day
| tuesday : day
| wednesday : day
| thursday : day
| friday : day
| saturday : day
| sunday : day.
可以这样解释这个定义:
定义一个参数类型为day,返回类型为day的函数:
Definition next_weekday (d:day) : day :=
match d with
| monday ⇒ tuesday
| tuesday ⇒ wednesday
| wednesday ⇒ thursday
| thursday ⇒ friday
| friday ⇒ monday
| saturday ⇒ monday
| sunday ⇒ monday
end.
注意这里虽然显式声明了参数类型和返回类型,但是coq也支持隐式声明(与大多数函数时编程语言类似)
定义好了一个函数后,coq中有3种不同的方式来检查它对于一些例子是否是有效的。其中第3方式是与其它编程语言相关的,有很大的用途,不过这里不介绍。
Compute (next_weekday friday).
(* ==> monday : day *)
Compute (next_weekday (next_weekday saturday)).
(* ==> tuesday : day *)
Example test_next_weekday:
(next_weekday (next_weekday saturday)) = tuesday.
上述声明可以理解为:创建一个名为test_next_weekday
内容为(next_weekday (next_weekday saturday)) = tuesday.
的断言。断言可以用下列语句来验证
Proof. simpl. reflexivity. Qed.
上述语句可以这样理解:
“The assertion we’ve just made can be proved by observing that both sides of the equality evaluate to the same thing, after some simplification.”
细节将在后续补充。
这里《Software Foundation》提供了从0开始的类型定义。但相同的定义可以在Coq.Init.Datatypes
中找到
布尔类型定义如下:
Inductive bool : Type :=
| true : bool
| false : bool.
布尔类型的一些函数(与或非)定义如下:
Definition negb (b:bool) : bool :=
match b with
| true ⇒ false
| false ⇒ true
end.
Definition andb (b1:bool) (b2:bool) : bool :=
match b1 with
| true ⇒ b2
| false ⇒ false
end.
Definition orb (b1:bool) (b2:bool) : bool :=
match b1 with
| true ⇒ true
| false ⇒ b2
end.
注意这里的andb
和orb
使用了两个参数。下面的‘单元测试’检验了orb
操作的真值表:
Example test_orb1: (orb true false) = true.
Proof. simpl. reflexivity. Qed.
Example test_orb2: (orb false false) = false.
Proof. simpl. reflexivity. Qed.
Example test_orb3: (orb false true) = true.
Proof. simpl. reflexivity. Qed.
Example test_orb4: (orb true true) = true.
Proof. simpl. reflexivity. Qed.
为了直观,如何对布尔值/变量进行"与或"操作定义专用的符号(&& || )呢?
Notation "x && y" := (andb x y).
Notation "x || y" := (orb x y).
Example test_orb5: false || false || true = true.
Proof. simpl. reflexivity. Qed.
占位符Admitted
可以用于略过一个未完成的证明
Coq中的每个表达式都是有类型的,可以用Check
命令打印表达式的类型
Check true.
(* ===> true : bool *)
Check (negb true).
(* ===> negb true : bool *)
Check negb.
(* ===> negb : bool -> bool *)
这里的negb
是函数类型,bool -> bool
表示对于给定的布尔类型输入,函数产生一个布尔类型的输出
。注意如果Check
的函数时andb
,则打印的内容为bool -> bool -> bool
,其中前2个bool
是输入。
到目前为止所有的Type都是枚举类型
,它们的定义明确枚举了一组有限的元素,每个元素都只是一个简单的构造器。我们也可以定义构造器接受参数的类型:
Inductive rgb : Type :=
| red : rgb
| green : rgb
| blue : rgb.
Inductive color : Type :=
| black : color
| white : color
| primary : rgb → color.
可以这么理解rgb
和color
类型的表达式的构建(build)过程:
本人理解水平有限,这里推荐去看原文
color
的函数定义类似于day
和bool
:
Definition monochrome (c : color) : bool :=
match c with
| black ⇒ true
| white ⇒ true
| primary p ⇒ false
end.
因为构造器primary
接受一个参数,参数的类型可以是变量(比如上面的primary p
或者常量:
Definition isred (c : color) : bool :=
match c with
| black ⇒ false
| white ⇒ false
| primary red ⇒ true
| primary _ ⇒ false
end.
这里的模式primary _
表示"接受除了red
以外的其它任何rgb构造器"。
可以用以下方法来定义模块X
:
Module X.
(**模块定义的内容**)
End X.
如果在模块X
中定义了类型foo
,那么出现在End X
之后的代码如果要使用foo
,应该引用为X.foo
。
定义一个类型还有一个更有趣的方法:允许类型的构造器接收与它同类型的参数,比如自然数(natural number, nat)的定义:
Inductive nat : Type :=
| O : nat
| S : nat → nat.
这个定义可以这么理解:
n
是一个自然数,那么S n
也是自然数目前为止nat的定义并没有什么具体的含义,它只是说明了“一个东西怎样才能算是nat”。在指定如何用它计算后,这些符号才解释的通,比如一个“前继”函数:
Definition pred (n : nat) : nat :=
match n with
| O ⇒ O
| S n' ⇒ n'
end.
因为自然数是表示数据的普遍形式,所以Coq提供了内置的分析和打印自然数的“魔法”(magic):普通的阿拉伯数字可以用作构造函数S和O定义的“一元”符号的替代。Coq默认可以打印成阿拉伯数字:
Check (S (S (S (S O)))).
(* ===> 4 : nat *)
Definition minustwo (n : nat) : nat :=
match n with
| O ⇒ O
| S O ⇒ O
| S (S n') ⇒ n'
end.
Compute (minustwo 4).
(* ===> 2 : nat *)
对于大多数作用域数字的函数,仅仅模式匹配是不够的,我们还需要用到递归。
比如判断自然数n是否是偶数:
Fixpoint evenb (n:nat) : bool :=
match n with
| O ⇒ true
| S O ⇒ false
| S (S n') ⇒ evenb n'
end.
然后可以用它来定义判断自然数n是否是奇数的函数:
Definition oddb (n:nat) : bool := negb (evenb n).
Example test_oddb1: oddb 1 = true.
Proof. simpl. reflexivity. Qed.
Example test_oddb2: oddb 4 = false.
Proof. simpl. reflexivity. Qed.
上面证明过程中simpl
实际上是没有影响的(以后将会详细说明)
同样的,可以定义多参数的递归函数:
Fixpoint plus (n : nat) (m : nat) : nat :=
match n with
| O ⇒ m
| S n' ⇒ S (plus n' m)
end.
为了方便,如果多个参数是相同类型的,可以写在一起:
Fixpoint mult (n m : nat) : nat :=
match n with
| O ⇒ O
| S n' ⇒ plus m (mult n' m)
end.
Example test_mult1: (mult 3 3) = 9.
Proof. simpl. reflexivity. Qed.
多个参数就会有多个匹配,匹配表达式可以用,
分开:
Fixpoint minus (n m:nat) : nat :=
match n, m with
| O , _ ⇒ O
| S _ , O ⇒ n
| S n', S m' ⇒ minus n' m'
end.
在Coq中我们可以自定义自然数的比较函数:
Fixpoint beq_nat (n m : nat) : bool :=
match n with
| O ⇒ match m with
| O ⇒ true
| S m' ⇒ false
end
| S n' ⇒ match m with
| O ⇒ false
| S m' ⇒ beq_nat n' m'
end
end.
下面是一个证明 0 + n = n
的定理
Theorem plus_O_n : forall n : nat, 0 + n = n.
Proof.
intros n. simpl. reflexivity. Qed.
上面的例子中 ,simpl
不是必需的,它只是为了方便查看简化的中间过程;reflexivity
在检查等号两边是否相等前可以自动完成一些简化工作。reflexivity
可以打开(unfolding)一些项,把这些项替换成定义时写在右边的东西。
有几点需要注意的:
Theorem
, Example
, Lemma
, Fact
, Remark
等区别不大。intros
把变量n
从量词forall
中移入到“当前的”状态中。intros
, simpl
, reflexivity
等都是tactics(策略),一个tactic是在Proof
和Qed
中用于引导证明过程的命令。这里原书建议运行一遍代码,看看Coq是如何一步步简化的。
Theorem plus_id_example : forall n m:nat,
n = m ->
n + n = m + m.
Proof.
(* move both quantifiers into the context: *)
intros n m.
(* move the hypothesis into the context: *)
intros H.
(* rewrite the goal using the hypothesis: *)
rewrite -> H.
reflexivity. Qed.
上面的->
读作“隐含/暗示”,n = m
为假设。因为n和m是任意数字,所以不能简单地用简化来证明该定理。在n = m
的假设下,我们把目标语句中的n替换成m即可完成证明。这种tactic叫做rewrite。
这里rewrite -> H.
表示通过把假设等式左边n
替换成右边m
来完成rewrite。如果要把等式右边的m
换成左边的n
,可以用<-
。
另外,rewrite的对象除了是假设,也可以说之前证明过的定理。如果引用的定理中含有量词,Coq就会尝试匹配当前目标来将其实例化
Theorem mult_0_plus : forall n m : nat,
(0 + n) * m = n * m.
Proof.
intros n m.
rewrite -> plus_O_n.
reflexivity. Qed.
尝试证明以下定理:
Theorem plus_1_neq_0_firsttry : forall n : nat,
beq_nat (n + 1) 0 = false.
回忆beq_nat
和plus
的定义,会发现如果想通过简化来证明该定理,对于变量n
,函数plus
在匹配时无法简化;而函数beq_nat
对于n+1
也无法简化(因为无法知道n
和n+1
“match”的是哪一种情况)
match n with
| O ⇒ match m with
| O ⇒ true
| S m' ⇒ false
end
| S n' ⇒ match m with
| O ⇒ false
| S m' ⇒ beq_nat n' m'
end
end.
Fixpoint plus (n : nat) (m : nat) : nat :=
match n with
| O ⇒ m
| S n' ⇒ S (plus n' m)
end.
那么如何解决这个问题呢?如果我们知道plus
“match”的是哪一种情况(O
或者S n'
),不就可以进行简化了吗?将变量分类分析的tactic叫destruct
:
Theorem plus_1_neq_0 : forall n : nat,
beq_nat (n + 1) 0 = false.
Proof.
intros n. destruct n as [| n'].
- reflexivity.
- reflexivity. Qed.
在destrcut
之后,就要分别证明了。分类证明可以用-
(书上叫做bullet
)来表示。对于两种情况(beq_nat (0 + 1) 0 = false
和beq_nat (S n' + 1) 0 = false
),都可以直接通过简化证明,所以直接reflexivity
即可。
这里的as [| n']
称为intro pattern
,它用于指示对于每个子目标,哪些新的变量名将被引进。通常,方括号中的内容是用|
分开的变量名列表。因为第一种情况下O
构造器不需要参数,s所以这里|
之前是空的;第二种情况S
构造器需要一个参数,所以填入一个参数n'
。如果任何类型的构造器都不需要参数,那么intro pattern
可以省略。
任何归纳定义的类型都可以用destruct
来分类分析。
有时候在证明一个子目标的同时还需要用到destruct
,这这时就需要用到不同等级的bullets
了
Theorem andb_commutative : forall b c, andb b c = andb c b.
Proof.
intros b c. destruct b.
- destruct c.
+ reflexivity.
+ reflexivity.
- destruct c.
+ reflexivity.
+ reflexivity.
Qed.
除了-
和+
,*
也可作为bullets
。此外,如果有更多等级,可以用花括号{}
来包含
Theorem andb_commutative' : forall b c, andb b c = andb c b.
Proof.
intros b c. destruct b.
{ destruct c.
{ reflexivity. }
{ reflexivity. } }
{ destruct c.
{ reflexivity. }
{ reflexivity. } }
Qed.
有了花括号之后,就可以在不同等级中多次使用相同的bullets
了。
有时候为了方便地在证明引进变量,可以直接忽略intros
,而直接destruct
:
Theorem plus_1_neq_0' : forall n : nat,
beq_nat (n + 1) 0 = false.
Proof.
intros [|n].
- reflexivity.
- reflexivity. Qed.
如果是多变量,则用多个[]
:
Theorem andb_commutative'' :
forall b c, andb b c = andb c b.
Proof.
intros [] [].
- reflexivity.
- reflexivity.
- reflexivity.
- reflexivity.
Qed.
回忆加号和乘号的引进:
Notation "x + y" := (plus x y)
(at level 50, left associativity)
: nat_scope.
Notation "x * y" := (mult x y)
(at level 40, left associativity)
: nat_scope.
其中:
at level n
标识了优先级,范围从0到100,数字越低,优先级越高left associativity
标识了结合方式,还可以是right associativity
或者no associativity
nat_scope
则显式地标识出这个notation的scope。这在打印的时候会有所不同。对于如下递归函数的定义:
Fixpoint plus' (n : nat) (m : nat) : nat :=
match n with
| O => m
| S n' => S (plus' n' m)
end.
Coq会检查它,并判断“这个函数的第1个参数总是减少的
”——即是说,参数n
在函数中一定是减少的,这隐含了“所有对plus'
的调用最终都会终止”这一条件。Coq要求每个Fixpoint
函数都必须要有至少一个这种减少的
参数。
但是Coq检查时并不是特别复杂,所以有时候为了通过检查,函数的写法要有一些不自然。