通过《Software Foundation》学习Coq语言的基本用法——1.Basics

摘录自《Software Foundation》,旨在交流与学习Coq的基本用法; 强烈建议大家下载源码自己运行一遍

文章目录

          • 用Inductive定义一个Type
          • 用Definition定义函数
          • 用Compute查看表达式的计算结果
          • 用Example检查计算结果是否符合预期
          • 布尔类型定义
          • 用Notation为现有的定义增加表示符号
          • 占位符Admitted
          • Check查看类型
          • 复合类型的定义
          • Module来定义模块
          • 归纳定义
          • 用Fixpoint定义递归函数
          • 通过Simplification证明
          • 通过rewrite证明
          • destruct:通过分类分析
          • 关于Notation的一些细节
          • 关于递归函数

用Inductive定义一个Type

定义一个类型(type):

Inductive day : Type :=
  | monday : day
  | tuesday : day
  | wednesday : day
  | thursday : day
  | friday : day
  | saturday : day
  | sunday : day.

可以这样解释这个定义:

  1. 这个类型叫做day
  2. 这个类型的成员是monday、tuesday等
  3. monday、tuesday后面的day可以这样理解:monday is a day, tuesday is a day …
用Definition定义函数

定义一个参数类型为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查看表达式的计算结果
Compute (next_weekday friday).
(* ==> monday : day *)
Compute (next_weekday (next_weekday saturday)).
(* ==> tuesday : day *)
用Example检查计算结果是否符合预期
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.

注意这里的andborb使用了两个参数。下面的‘单元测试’检验了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为现有的定义增加表示符号
Notation "x && y" := (andb x y).
Notation "x || y" := (orb x y).
Example test_orb5: false || false || true = true.
Proof. simpl. reflexivity. Qed.
占位符Admitted

占位符Admitted可以用于略过一个未完成的证明

Check查看类型

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.

可以这么理解rgbcolor类型的表达式的构建(build)过程:

本人理解水平有限,这里推荐去看原文

  1. red, green, blue是rgb的构造器,black, white, primary是color的构造器
  2. 表达式red, green, blue属于类型rgb,表达式black, white属于类型color
  3. 如果p是一个属于类型rgb的表达式,那么primary p(称作“参数为p的构造器primary”)是一个属于类型color的表达式

color的函数定义类似于daybool

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构造器"。

Module来定义模块

可以用以下方法来定义模块X:

Module X.

(**模块定义的内容**)

End X.

如果在模块X中定义了类型foo,那么出现在End X之后的代码如果要使用foo,应该引用为X.foo

归纳定义

定义一个类型还有一个更有趣的方法:允许类型的构造器接收与它同类型的参数,比如自然数(natural number, nat)的定义:

Inductive nat : Type :=
  | O : nat
  | S : nat → nat.

这个定义可以这么理解:

  1. O是一个自然数(注意是字母O不是数字0)
  2. S可以放在一个自然数前面,并表示另一个自然数。也即是说,如果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 *)

对于大多数作用域数字的函数,仅仅模式匹配是不够的,我们还需要用到递归。

用Fixpoint定义递归函数

比如判断自然数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.
通过Simplification证明

下面是一个证明 0 + n = n的定理

Theorem plus_O_n : forall n : nat, 0 + n = n.
Proof.
  intros n. simpl. reflexivity.  Qed.

上面的例子中 ,simpl不是必需的,它只是为了方便查看简化的中间过程;reflexivity在检查等号两边是否相等前可以自动完成一些简化工作。reflexivity可以打开(unfolding)一些项,把这些项替换成定义时写在右边的东西。

有几点需要注意的:

  • 在Coq中,Theorem, Example, Lemma, Fact, Remark等区别不大。
  • intros把变量n从量词forall中移入到“当前的”状态中。
  • 关键词intros, simpl, reflexivity等都是tactics(策略),一个tactic是在ProofQed中用于引导证明过程的命令。

这里原书建议运行一遍代码,看看Coq是如何一步步简化的。

通过rewrite证明
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.
destruct:通过分类分析

尝试证明以下定理:

Theorem plus_1_neq_0_firsttry : forall n : nat,
  beq_nat (n + 1) 0 = false.

回忆beq_natplus的定义,会发现如果想通过简化来证明该定理,对于变量n,函数plus在匹配时无法简化;而函数beq_nat对于n+1也无法简化(因为无法知道nn+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 = falsebeq_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的一些细节

回忆加号和乘号的引进:

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检查时并不是特别复杂,所以有时候为了通过检查,函数的写法要有一些不自然。

你可能感兴趣的:(课程相关)