通过《Software Foundation》学习Coq语言的基本用法——2.Induction 3.Lists

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

文章目录

  • Induction
          • 通过Simplification证明的局限性
          • 用induction进行归纳证明
          • 用assert证明局部的引理
  • Lists
          • natprod类型的定义
          • natlist类型的定义
          • 一些函数的定义
          • 使用条件表达式if

Induction

通过Simplification证明的局限性

前面已经通过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在这里无能为力,因为nn + 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.

运行以上代码会发现,destructn分为O(也就是0)与S n'(也就是非0)两种情况。n为0时可以证明,但n = S n'时仍然证明不了——即使将n'destruct

回忆数学归纳法的使用。Coq中也提供了类似的证明方法。

用induction进行归纳证明

注意这里的induction不是定义类型的inducive

Coq中提供了一个叫induction的tactic,它把上述定理的证明分成两个子目标:

  1. 证明P(O)成立
  2. 证明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来设定两个子目标(用|分开)。

  1. 第一个子目标中n被替换成O,所以不需要引入新的变量
  2. 第二个子目标中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

用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,其中的nm是就是mult_0_plus中的nm

这里局部引理的另一个好处可以看出来了:它可以直接作用于证明过程中的“局部变量”


Lists

natprod类型的定义

回忆用inductive定义的类型,它们的每个构造器可以接收任何数量的参数,比如trueO接收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类型的定义

下面我们尝试定义类型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里面。

使用条件表达式if
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支持任何有两个构造器的类型作为条件表达式,如果计算结果是第一个构造器则条件表达式的结果为真,否则条件表达式的结果为假。

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