LogicCoq 中的逻辑系统
(* TODO(OlingCat): 需要统一 claim、statement 的译法。 *)
Set Warnings "-notation-overridden,-parsing".
From LF Require Export Tactics.
我们已经见过很多对事实的断言(即'命题')
以及如何用证据展示其正确性(即'证明')的例子了。特别是,
我们证明了大量的'相等性命题'(e1 = e2)、蕴含式(P → Q)和量化命题
(∀ x, P x)。在本章中,我们将会看到如何用 Coq 解决类似形式的逻辑推理。
在深入细节之前,我们先来探讨一下 Coq 中数学表达式的地位。
回忆一下,Coq 是一门拥有'类型'的语言,也就是说,一切有意义的
表达式都具有一个相应的类型。逻辑表达也不例外,我们试图在 Coq
中证明的一切语句都有名为 Prop 的类型,即'命题类型'。我们
可以用 Check 指令来查看:
注意:'所有'语法形式良好的命题,无论是否为真,其类型均为 Prop。
简单来说,'是'一个命题与该命题'可以证明'是两回事。
除了拥有类型之外,命题还是'一等的(First-Class)'实体,
即在 Coq 的世界中,我们可以像操作其它实体那样操作命题。
到目前为止,我们已经知道命题可以出现在 Theorem(还有 Lemma 以及
Example)的声明中了。
不过命题还可以用在其它地方。例如,我们可以用 Definition
为命题取名,就像为其它表达式取名一样。
之后我们可以在任何需要此命题的地方使用它们名字——例如,作为一个
Theorem 声明中的断言:
我们也可以写出'参数化'的命题 -- 也就是一个接受某些类型的参数,
然后返回一个命题的函数。
例如,以下函数接受某个数字,返回一个命题断言该数字等于 3:
在 Coq 中,返回命题的函数可以说是定义了其参数的'性质'。
例如,以下(多态的)性质定义了常见的 '单射函数' 的概念。
Definition injective {A B} (f : A → B) :=
∀ x y : A, f x = f y → x = y.
Lemma succ_inj : injective S.
Proof.
intros n m H. injection H as H1. apply H1.
Qed.
相等关系运算符 = 也是一个返回 Prop 的函数。
表达式 n = m 只是 eq n m 的语法糖(它使用 Notation
机制定义在 Coq 标准库中)。由于 eq 可被用于任何类型的元素,因此它也是多态的:
(注意我们写的是 @eq 而非 eq:eq 的类型参数 A
是隐式声明的,因此我们需要关掉隐式参数的类型推断以便看到 eq 的完整类型。)
证明合取的命题通常使用 split 策略。它会分别为语句的两部分生成两个子目标:
Proof.
split.
- (* 3 + 4 = 7 *) reflexivity.
- (* 2 * 2 = 4 *) reflexivity.
Qed.
对于任意命题 A 和 B,如果我们假设 A 为真且 B 为真,
那么就能得出 A ∧ B 也为真的结论。
Lemma and_intro : ∀ A B : Prop, A → B → A ∧ B.
Proof.
intros A B HA HB. split.
- apply HA.
- apply HB.
Qed.
由于按照前提对某个目标应用定理会产生与该定理的前提一样多的子目标。
因此我们可以应用 and_intro 来达到和 split 一样的效果。
Example and_example' : 3 + 4 = 7 ∧ 2 × 2 = 4.
Proof.
apply and_intro.
- (* 3 + 4 = 7 *) reflexivity.
- (* 2 + 2 = 4 *) reflexivity.
Qed.
Lemma and_example2 :
∀ n m : nat, n = 0 ∧ m = 0 → n + m = 0.
Proof.
(* 课上已完成 *)
intros n m H.
destruct H as [Hn Hm] eqn:HE.
rewrite Hn. rewrite Hm.
reflexivity.
Qed.
和往常一样,我们也可以在引入 H 的同时对其进行解构,
而不必先引入然后再解构:
Lemma and_example2' :
∀ n m : nat, n = 0 ∧ m = 0 → n + m = 0.
Proof.
intros n m [Hn Hm].
rewrite Hn. rewrite Hm.
reflexivity.
Qed.
为什么我们要麻烦地将 n = 0 和 m = 0 这两个前提放一条合取语句中呢?
完全可以用两条独立的前提来陈述此定理啊:
Lemma and_example2'' :
∀ n m : nat, n = 0 → m = 0 → n + m = 0.
Proof.
intros n m Hn Hm.
rewrite Hn. rewrite Hm.
reflexivity.
Qed.
就此定理而言,两种方式都可以。不过理解如何证明合取前提非常重要,
因为合取语句通常会在证明的中间步骤中出现,特别是在做大型开发的时候。
下面是个简单的例子:
Lemma and_example3 :
∀ n m : nat, n + m = 0 → n × m = 0.
Proof.
(* 课上已完成 *)
intros n m H.
assert (H' : n = 0 ∧ m = 0).
{ apply and_exercise. apply H. }
destruct H' as [Hn Hm] eqn:HE.
rewrite Hn. reflexivity.
Qed.
另一种经常遇到合取语句的场景是,我们已经知道了 A ∧ B,
但在某些上下文中只需要 A 或者 B。此时我们可以用 destruct
进行解构(或许是作为 intros 的一部分)并用下划线模式
_ 来丢弃不需要的合取分式。
Lemma proj1 : ∀ P Q : Prop,
P ∧ Q → P.
Proof.
intros P Q HPQ.
destruct HPQ as [HP _].
apply HP. Qed.
Theorem and_commut : ∀ P Q : Prop,
P ∧ Q → Q ∧ P.
Proof.
intros P Q [HP HQ].
split.
- (* left *) apply HQ.
- (* right *) apply HP. Qed.
练习:2 星, standard (and_assoc)
(在以下结合律的证明中,注意'嵌套'的 intros 模式是如何将 H : P ∧ (Q ∧ R) 拆分为 HP : P、HQ : Q 和 HR : R 的。 请从那里开始完成证明。)Theorem and_assoc : ∀ P Q R : Prop,
P ∧ (Q ∧ R) → (P ∧ Q) ∧ R.
Proof.
intros P Q R [HP [HQ HR]].
(* 请在此处解答 *) Admitted.
☐
析取
Lemma eq_mult_0 :
∀ n m : nat, n = 0 ∨ m = 0 → n × m = 0.
Proof.
(* Hn | Hm 会隐式地对 n = 0 ∨ m = 0 进行分类讨论 *)
intros n m [Hn | Hm].
- (* 在这里 n = 0 *)
rewrite Hn. reflexivity.
- (* 在这里 m = 0 *)
rewrite Hm. rewrite <- mult_n_O.
reflexivity.
Qed.
相应地,要证明某个析取命题成立,只需证明其任意一边的命题成立就够了。
我们可以用 left 和 right 策略来选取命题。顾名思义,left
会选取待析取证命题的左边,而 right 则会选取它的右边。
下面是一种平凡的用法...
...而这个更有趣的例子则同时需要 left 和 right:
Lemma zero_or_succ :
∀ n : nat, n = 0 ∨ n = S (pred n).
Proof.
(* 课上已完成 *)
intros [|n'].
- left. reflexivity.
- right. reflexivity.
Qed.
假命题与否定
目前为止,我们主要都在证明某些东西是'真'的:加法满足结合律, 列表的连接满足结合律,等等。当然,我们也关心'否定'的结果, 即证明某些给定的命题'不是'真的。在 Coq 中,这样的否定语句使用逻辑否定运算符 ¬ 来表达。Module MyNot.
Definition not (P:Prop) := P → False.
Notation "~ x" := (not x) : type_scope.
Check not : Prop → Prop.
End MyNot.
由于 False 是个矛盾性命题,因此爆炸原理对它也适用。如果我们让 False
进入到了证明的上下文中,可以对它使用 destruct 来完成任何待证目标。
Theorem ex_falso_quodlibet : ∀ (P:Prop),
False → P.
Proof.
(* 课上已完成 *)
intros P contra.
destruct contra. Qed.
拉丁文 'ex falso quodlibet' 的字面意思是“从谬误出发,
你能够证明任何你想要的”,这也是爆炸原理的另一个广为人知的名字。
练习:2 星, standard, optional (not_implies_our_not)
证明 Coq 对否定的定义蕴含前面提到的直觉上的定义:Notation "x <> y" := (~(x = y)).
性质 0 ≠ 1 就是 ~(0 = 1),即 not (0 = 1),
它会展开为 (0 = 1) → False。(这里显式地用 unfold not
展示了这一点,不过一般可以忽略。
要证明不等性,我们可以反过来假设其相等...
intros contra.
... 然后从中推出矛盾。在这里,等式 O = S O 与构造子 O 和 S
的不交性相矛盾,因此用 discriminate 就能解决它。
discriminate contra.
Qed.
为了习惯用 Coq 处理否定命题,我们需要一些练习。
即便你十分清楚为什么某个否定命题成立,但能让 Coq 一下就理解则需要点小技巧。
以下常见事实的证明留给你热身。
Theorem not_False :
¬ False.
Proof.
unfold not. intros H. destruct H. Qed.
Theorem contradiction_implies_anything : ∀ P Q : Prop,
(P ∧ ¬P) → Q.
Proof.
(* 课上已完成 *)
intros P Q [HP HNA]. unfold not in HNA.
apply HNA in HP. destruct HP. Qed.
Theorem double_neg : ∀ P : Prop,
P → ~~P.
Proof.
(* 课上已完成 *)
intros P H. unfold not. intros G. apply G. apply H. Qed.
(* 请在此处解答 *)
(* 请勿修改下面这一行: *)
Definition manual_grade_for_double_neg_inf : option (nat×string) := None.
☐
(* 请在此处解答 *)
(* 请勿修改下面这一行: *)
Definition manual_grade_for_informal_not_PNP : option (nat×string) := None.
☐
Theorem not_true_is_false : ∀ b : bool,
b ≠ true → b = false.
Proof.
intros b H.
destruct b eqn:HE.
- (* b = true *)
unfold not in H.
apply ex_falso_quodlibet.
apply H. reflexivity.
- (* b = false *)
reflexivity.
Qed.
由于用 ex_falso_quodlibet 推理十分常用,因此 Coq 提供了内建的策略
exfalso。
Theorem not_true_is_false' : ∀ b : bool,
b ≠ true → b = false.
Proof.
intros [] H. (* note implicit destruct b here *)
- (* b = true *)
unfold not in H.
exfalso. (* <=== *)
apply H. reflexivity.
- (* b = false *) reflexivity.
Qed.
与经常使用的 False 不同,True 很少使用,因为它作为证明目标来说过于平凡,
而作为前提又不携带任何有用的信息。 然而在使用条件从句定义复杂的 Prop,或者作为高阶 Prop 的参数时,
它还是挺有用的。之后我们会看到一些例子。
联结词“当且仅当”用起来十分方便,它是两个蕴含式的合取,
断言了两个命题拥有同样的真值。
逻辑等价
Module MyIff.
Definition iff (P Q : Prop) := (P → Q) ∧ (Q → P).
Notation "P <-> Q" := (iff P Q)
(at level 95, no associativity)
: type_scope.
End MyIff.
Theorem iff_sym : ∀ P Q : Prop,
(P ↔ Q) → (Q ↔ P).
Proof.
(* 课上已完成 *)
intros P Q [HAB HBA].
split.
- (* -> *) apply HBA.
- (* <- *) apply HAB. Qed.
Lemma not_true_iff_false : ∀ b,
b ≠ true ↔ b = false.
Proof.
(* 课上已完成 *)
intros b. split.
- (* -> *) apply not_true_is_false.
- (* <- *)
intros H. rewrite H. intros H'. discriminate H'.
Qed.
Theorem or_distributes_over_and : ∀ P Q R : Prop,
P ∨ (Q ∧ R) ↔ (P ∨ Q) ∧ (P ∨ R).
Proof.
(* 请在此处解答 *) Admitted.
☐
P ∨ (Q ∧ R) ↔ (P ∨ Q) ∧ (P ∨ R).
Proof.
(* 请在此处解答 *) Admitted.
☐
广集与逻辑等价
“广集(Setoid)”指配备了等价关系的集合,即满足自反性、对称性和传递性的关系。
当一个集合中的两个元素在这种关系上等价时,可以用 rewrite
将其中一个元素替换为另一个。我们已经在 Coq 中见过相等性关系 = 了:
当 x = y 时,我们可以用 rewrite 将 x 替换为 y,反之亦可。
同样,逻辑等价关系 ↔ 也满足自反性、对称性和传递性,
因此我们可以用它将替换命题中的一部分替换为另一部分:若
P ↔ Q,那么我们可以用 rewrite 将 P 替换为 Q,反之亦可。
下面是一个简单的例子,它展示了这些策略如何使用 iff。
首先,我们来证明一些基本的 iff 等价关系命题...
Lemma mult_0 : ∀ n m, n × m = 0 ↔ n = 0 ∨ m = 0.
Lemma or_assoc :
∀ P Q R : Prop, P ∨ (Q ∨ R) ↔ (P ∨ Q) ∨ R.
Proof.
intros P Q R. split.
- intros [H | [H | H]].
+ left. left. apply H.
+ left. right. apply H.
+ right. apply H.
- intros [[H | H] | H].
+ left. apply H.
+ right. left. apply H.
+ right. right. apply H.
Qed.
intros P Q R. split.
- intros [H | [H | H]].
+ left. left. apply H.
+ left. right. apply H.
+ right. apply H.
- intros [[H | H] | H].
+ left. apply H.
+ right. left. apply H.
+ right. right. apply H.
Qed.
现在我们可以用这些事实配合 rewrite 与 reflexivity
对涉及等价关系的陈述给出流畅的证明了。以下是之前 mult_0
包含三个变量的版本:
Lemma mult_0_3 :
∀ n m p, n × m × p = 0 ↔ n = 0 ∨ m = 0 ∨ p = 0.
Proof.
intros n m p.
rewrite mult_0. rewrite mult_0. rewrite or_assoc.
reflexivity.
Qed.
apply 策略也可以用在 ↔ 上。当给定一个等价关系命题作为
apply 的参数时,它会试图猜出正确的方向。
Lemma apply_iff_example :
∀ n m : nat, n × m = 0 → n = 0 ∨ m = 0.
Proof.
intros n m H. apply mult_0. apply H.
Qed.
存在量化
Definition even x := ∃ n : nat, x = double n.
Lemma four_is_even : even 4.
Proof.
unfold even. ∃ 2. reflexivity.
Qed.
反之,如果我们的的上下文中有形如 ∃ x, P 的存在前提,
可以将其解构得到一个例证 x 和一个陈述 P 对于 x 成立的前提。
Theorem exists_example_2 : ∀ n,
(∃ m, n = 4 + m) →
(∃ o, n = 2 + o).
Proof.
(* 课上已完成 *)
intros n [m Hm]. (* 注意这里隐式使用了 destruct *)
∃ (2 + m).
apply Hm. Qed.
练习:1 星, standard, recommended (dist_not_exists)
请证明“P 对所有 x 成立”蕴含“不存在 x 使 P 不成立。” (提示:destruct H as [x E] 可以用于存在假设!)Theorem dist_not_exists : ∀ (X:Type) (P : X → Prop),
(∀ x, P x) → ¬ (∃ x, ¬ P x).
Proof.
(* 请在此处解答 *) Admitted.
☐
Theorem dist_exists_or : ∀ (X:Type) (P Q : X → Prop),
(∃ x, P x ∨ Q x) ↔ (∃ x, P x) ∨ (∃ x, Q x).
Proof.
(* 请在此处解答 *) Admitted.
☐
使用命题编程
- 若 l 为空列表,则 x 无法在其中出现,因此性质“x 出现在 l 中” 为假。
- 否则,若 l 的形式为 x' :: l',此时 x 是否出现在 l 中, 取决于它是否等于 x' 或出现在 l' 中。
Fixpoint In {A : Type} (x : A) (l : list A) : Prop :=
match l with
| [] ⇒ False
| x' :: l' ⇒ x' = x ∨ In x l'
end.
当 In 应用于具体的列表时,它会被展开为一系列具体的析取式。
Example In_example_1 : In 4 [1; 2; 3; 4; 5].
Proof.
(* 课上已完成 *)
simpl. right. right. right. left. reflexivity.
Qed.
Example In_example_2 :
∀ n, In n [2; 4] →
∃ n', n = 2 × n'.
Proof.
(* 课上已完成 *)
simpl.
intros n [H | [H | []]].
- ∃ 1. rewrite <- H. reflexivity.
- ∃ 2. rewrite <- H. reflexivity.
Qed.
(注意我们用空模式'无视'了最后一种情况。)
我们也可证明关于 In 的更一般,更高阶的引理。
注意,首先 In 会被应用到一个变量上,只有当我们对它进行分类讨论时,
它才会被展开:
Lemma In_map :
∀ (A B : Type) (f : A → B) (l : list A) (x : A),
In x l →
In (f x) (map f l).
Proof.
intros A B f l x.
induction l as [|x' l' IHl'].
- (* l = nil,矛盾 *)
simpl. intros [].
- (* l = x' :: l' *)
simpl. intros [H | H].
+ rewrite H. left. reflexivity.
+ right. apply IHl'. apply H.
Qed.
虽然递归定义命题在某些情况下会很方便,但这种方式也有其劣势。特别是,
这类命题会受到 Coq 对递归函数要求的限制,例如,在 Coq 中递归函数必须是
“明显会终止”的。在下一章中,我们会了解如何'归纳地'定义命题,
这是一种与之不同的技巧,有着其独特的优势和限制。
练习:3 星, standard (In_map_iff)
Lemma In_map_iff :
∀ (A B : Type) (f : A → B) (l : list A) (y : B),
In y (map f l) ↔
∃ x, f x = y ∧ In x l.
Proof.
intros A B f l y. split.
(* 请在此处解答 *) Admitted.
☐
∀ (A B : Type) (f : A → B) (l : list A) (y : B),
In y (map f l) ↔
∃ x, f x = y ∧ In x l.
Proof.
intros A B f l y. split.
(* 请在此处解答 *) Admitted.
☐
Lemma In_app_iff : ∀ A l l' (a:A),
In a (l++l') ↔ In a l ∨ In a l'.
Proof.
intros A l. induction l as [|a' l' IH].
(* 请在此处解答 *) Admitted.
☐
In a (l++l') ↔ In a l ∨ In a l'.
Proof.
intros A l. induction l as [|a' l' IH].
(* 请在此处解答 *) Admitted.
☐
练习:3 星, standard, recommended (All)
回忆一下,返回命题的函数可以视作对其参数'性质'的定义。例如,若 P 的类型为 nat → Prop,那么 P n 就陈述了性质 P 对 n 成立。Fixpoint All {T : Type} (P : T → Prop) (l : list T) : Prop
(* 将本行替换成 ":= _你的_定义_ ." *). Admitted.
Lemma All_In :
∀ T (P : T → Prop) (l : list T),
(∀ x, In x l → P x) ↔
All P l.
Proof.
(* 请在此处解答 *) Admitted.
☐
练习:3 星, standard (combine_odd_even)
完成以下 combine_odd_even 函数的定义。它接受两个对数字成立的性质 Podd 与 Peven,返回性质 P 使得当 n 为奇数时 P n 等价于 Podd n, 否则等价于 Peven n。Definition combine_odd_even (Podd Peven : nat → Prop) : nat → Prop
(* 将本行替换成 ":= _你的_定义_ ." *). Admitted.
为了测试你的定义,请证明以下事实:
Theorem combine_odd_even_intro :
∀ (Podd Peven : nat → Prop) (n : nat),
(oddb n = true → Podd n) →
(oddb n = false → Peven n) →
combine_odd_even Podd Peven n.
Proof.
(* 请在此处解答 *) Admitted.
Theorem combine_odd_even_elim_odd :
∀ (Podd Peven : nat → Prop) (n : nat),
combine_odd_even Podd Peven n →
oddb n = true →
Podd n.
Proof.
(* 请在此处解答 *) Admitted.
Theorem combine_odd_even_elim_even :
∀ (Podd Peven : nat → Prop) (n : nat),
combine_odd_even Podd Peven n →
oddb n = false →
Peven n.
Proof.
(* 请在此处解答 *) Admitted.
☐
对参数应用定理
在检查定理 plus_comm 的'陈述'时,Coq 使用了与检查某项的'类型'一样的方式
(如果我们保留以冒号开始的部分,那么它会被打印出来)。这是为什么?
原因在于标识符 plus_comm 其实指代的是被称作'证明对象'的数据结构,
它表示在命题 ∀ n m : nat, n + m = m + n 的真实性上建立的逻辑推导。
此对象的类型'就是'其所证命题的陈述。
从直觉上来说,这很有道理,因为对定理的陈述说明了该定理可用来做什么,
正如可计算对象的类型告诉了我们可以对它做什么。例如,若我们有一个类型为
nat → nat → nat 的项,就可以给它两个 nat 作为参数并得到一个 nat。
类似地,如果我们有一个类型为 n = m → n + n = m + m 的对象,
就能为它提供一个类型为 n = m 的“参数”并推导出 n + n = m + m。
从操作上来说,这种类比可以更进一步:由于定理可以作为函数,
被应用到对应类型的前提上,因此我们可以直接产生结论而不必在途中使用断言。
例如,假设我们想要证明以下结论:
乍看起来,我们似乎可以用 plus_comm 改写两次使两边匹配来证明它。
然而问题是,第二次 rewrite 会抵消第一次的效果。
我们之前在 Induction 一章中见过类似的问题,绕过它的一种简单方法是使用
assert 导出 plus_comm 的特殊版本,这样我们就能用它按照预期来改写。
Lemma plus_comm3_take2 :
∀ x y z, x + (y + z) = (z + y) + x.
Proof.
intros x y z.
rewrite plus_comm.
assert (H : y + z = z + y).
{ rewrite plus_comm. reflexivity. }
rewrite H.
reflexivity.
Qed.
一种更优雅的方式是直接把我们想要实例化的参数应用到 plus_comm 上,
就像我们将一个多态函数应用到类型参数上那样。
Lemma plus_comm3_take3 :
∀ x y z, x + (y + z) = (z + y) + x.
Proof.
intros x y z.
rewrite plus_comm.
rewrite (plus_comm y z).
reflexivity.
Qed.
我们来看看另一个像函数那样使用定理或引理的例子。
以下定理说明:任何包含元素的列表 l 一定非空。
Theorem in_not_nil :
∀ A (x : A) (l : list A), In x l → l ≠ [].
Proof.
intros A x l H. unfold not. intro Hl. destruct l eqn:HE.
- simpl in H. destruct H.
- discriminate Hl.
Qed.
有趣的地方是一个量化的变量(x)没有出现在结论(l ≠ [])中。
我们可以用此引理来证明 x 为 42 的特殊情况。直接用 apply in_not_nil
会失败,因为它无法推出 x 的值。
Lemma in_not_nil_42 :
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
Fail apply in_not_nil.
Abort.
有一些方法可以绕开它:
Use apply ... with ...
Lemma in_not_nil_42_take2 :
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply in_not_nil with (x := 42).
apply H.
Qed.
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply in_not_nil with (x := 42).
apply H.
Qed.
Use apply ... in ...
Lemma in_not_nil_42_take3 :
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply in_not_nil in H.
apply H.
Qed.
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply in_not_nil in H.
apply H.
Qed.
显式地对 x 的值应用引理。
Lemma in_not_nil_42_take4 :
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply (in_not_nil nat 42).
apply H.
Qed.
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply (in_not_nil nat 42).
apply H.
Qed.
显式地对假设应用引理。
Lemma in_not_nil_42_take5 :
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply (in_not_nil _ _ _ H).
Qed.
∀ l : list nat, In 42 l → l ≠ [].
Proof.
intros l H.
apply (in_not_nil _ _ _ H).
Qed.
对于几乎所有将定理名作为参数的策略而言,你都可以“将定理作为函数”来使用。
注意,定理应用与函数应用使用了同样的类型推导机制,所以你可以将通配符作为定理的参数,
或者为定理声明默认的隐式前提。这些特性在以下证明中展示。(此证明如何工作的细节
不必关心,这里的目标只是为了展示它的用途。)
Example lemma_application_ex :
∀ {n : nat} {ns : list nat},
In n (map (fun m ⇒ m × 0) ns) →
n = 0.
Proof.
intros n ns H.
destruct (proj1 _ _ (In_map_iff _ _ _ _ _) H)
as [m [Hm _]].
rewrite mult_0_r in Hm. rewrite <- Hm. reflexivity.
Qed.
在以后的章节中我们将会看到更多这方面的例子。
Coq vs. 集合论
函数的外延性
在一般的数学研究中,对于任意两个函数 f 和 g,
只要它们对相同的输入产生相等的结果,那么它们就被认为相等:
(∀ x, f x = g x) → f = g
这被称作'函数的外延性原理'。
不甚严谨地说,所谓“外延性”是指某个对象可观察到的行为。
因此,函数的外延性就是指函数的标识完全由其行为来决定。
用 Coq 的术语来说,就是函数的身份视其被应用后的结果而定。
然而,函数的外延性并不在 Coq 的基本公理之内,因此某些“合理”的命题是不可证明的:
(∀ x, f x = g x) → f = g
不过我们可以用 Axiom 指令将函数的外延性添加到 Coq 的核心逻辑系统中。
将某个东西用 Axiom 定义为公理的效果与陈述一个定理并用 Admitted 跳过其证明相同,
不过它会提醒读者这是一个公理,我们无需证明!
现在我们可以在证明中调用函数的外延性了:
Example function_equality_ex2 :
(fun x ⇒ plus x 1) = (fun x ⇒ plus 1 x).
Proof.
apply functional_extensionality. intros x.
apply plus_comm.
Qed.
当然,在为 Coq 添加公理时必须十分小心,因为这有可能会导致系统
'不一致',而当系统不一致的,任何命题都能在其中证明,包括 False
和 2+2=5!
不幸的是,并没有一种简单的方式能够判断添加某条公理是否安全:
一般来说,确认任何一组公理的一致性都需要训练有素的数学家付出艰辛的努力。
然而,我们已经知道了添加函数外延性后的公理系统'确实是'一致的。
我们可以用 Print Assumptions 指令查看某个证明依赖的所有附加公理。
Print Assumptions function_equality_ex2.
(* ===>
Axioms:
functional_extensionality :
forall (X Y : Type) (f g : X -> Y),
(forall x : X, f x = g x) -> f = g *)
练习:4 星, standard (tr_rev_correct)
列表反转函数 rev 的定义有一个问题,它会在每一步都执行一次 app 调用,而运行 app 所需时间与列表的大小线性渐近,也就是说 rev 的时间复杂度与列表长度呈渐进平方关系。我们可以用以下定义来改进它:Fixpoint rev_append {X} (l1 l2 : list X) : list X :=
match l1 with
| [] ⇒ l2
| x :: l1' ⇒ rev_append l1' (x :: l2)
end.
Definition tr_rev {X} (l : list X) : list X :=
rev_append l [].
此版本的 rev 是'尾递归(tail-recursive)'的,
因为对函数自身的递归调用是需要执行的最后一步操作
(即,在递归调用之后我们并不执行 ++ )。
一个足够好的编译器会为此生成十分高效的代码。
请证明以下两个定义等价。
命题 vs. 布尔值
或者存在某个 k 使得 n = double k:
当然,如果二者刻画的偶数性描述的不是同一个自然数集合,那么会非常奇怪!
幸运的是,我们确实可以证明二者相同...
首先我们需要两个辅助引理。
Lemma evenb_double : ∀ k, evenb (double k) = true.
Proof.
intros k. induction k as [|k' IHk'].
- reflexivity.
- simpl. apply IHk'.
Qed.
intros k. induction k as [|k' IHk'].
- reflexivity.
- simpl. apply IHk'.
Qed.
Lemma evenb_double_conv : ∀ n, ∃ k,
n = if evenb n then double k else S (double k).
n = if evenb n then double k else S (double k).
Proof.
(* 提示:使用 Induction.v 中的 evenb_S 引理。 *)
(* 请在此处解答 *) Admitted.
☐
(* 提示:使用 Induction.v 中的 evenb_S 引理。 *)
(* 请在此处解答 *) Admitted.
☐
Now the main theorem:
Theorem even_bool_prop : ∀ n,
evenb n = true ↔ even n.
evenb n = true ↔ even n.
Proof.
intros n. split.
- intros H. destruct (evenb_double_conv n) as [k Hk].
rewrite Hk. rewrite H. ∃ k. reflexivity.
- intros [k Hk]. rewrite Hk. apply evenb_double.
Qed.
intros n. split.
- intros H. destruct (evenb_double_conv n) as [k Hk].
rewrite Hk. rewrite H. ∃ k. reflexivity.
- intros [k Hk]. rewrite Hk. apply evenb_double.
Qed.
此定理说明,布尔计算 evenb n 的真假会从命题 ∃ k, n = double k
的真假中'反映(reflected)'出来。
类似地,以下两种 n 与 m 相等的表述等价:
- (1) n =? m 值为 true;
- (2) n = m。
然而,即便布尔值和命题式在逻辑上是等价的,
但它们的方便性从某些特定的目上来看并不一样。
在前面的偶数例子中,证明 even_bool_prop 的反向部分(即
evenb_double,从命题到布尔表达式的方向)时,我们对
k 进行了简单的归纳。而反方向的证明(即练习 evenb_double_conv)
则需要一种聪明的一般化方法,因为我们无法直接证明
(evenb n = true) → even n。
对于这些例子来说,命题式的声明比与之对应的布尔表达式要更为有用,
但并非总是如此。例如,我们无法在函数的定义中测试一般的命题是否为真,
因此以下代码片段会被拒绝:
Coq 会抱怨 n = 2 的类型是 Prop,而它想要一个 bool
类型的元素(或其它带有两个元素的归纳类型)。原因与 Coq
核心语言的'可计算性'特质有关,即它能表达的所有函数都是可计算且完全的。
这样设计的的原因之一是为了能从 Coq 开发的代码中提取出可执行程序。
因此,在 Coq 中 Prop '并没有'一种通用的情况分析操作来确定
任意给定的命题是否为真,一旦存在这种操作,我们就能写出不可计算的函数。
尽管一般的不可计算性质无法表述为布尔计算,但值得注意的是,很多
'可计算的'性质更容易通过 Prop 而非 bool 来表达,因为在 Coq
中定义递归函数中会受到很大的限制。例如,下一章会展示如何用 Prop
来定义“某个正则表达式可以匹配给定的字符串”这一性质。如果使用
bool 来定义,就需要写一个真正的正则表达式匹配器了,比起该性质的简单定义
(即非算法的定义)来说,这样会更加复杂,更难以理解,也更难以对它进行推理。
另一方面,通过布尔值来陈述事实会带来一点重要的优势,即通过对 Coq
中的项进行计算可以实现一些自动推理,这种技术被称为'互映证明(Proof
by Reflection)'。考虑以下陈述:
对此命题而言,最直接的证明方式就是直接给出 k 的值。
而使用与之对应的布尔语句的证明则更加简单(因为我们不必给出证据,Coq
的计算机制会帮我们搞定它!)
有趣的是,由于这两种定义是等价的,因此我们无需显式地给出 500,
而是使用布尔等价式来证明彼此:
尽管此例的证明脚本的长度并未因此而减少,然而更大的证明通常可通过
这种互映的方式来显著化简。举一个极端的例子,在用 Coq 证明著名的
'四色定理'时,人们使用互映技巧将几百种不同的情况归约成了一个布尔计算。
另一点明显的不同是“布尔事实”的否定可以被直白地陈述并证明,
只需翻转预期的布尔值结果即可。
相反,命题的否定形式可能更难以直接证明。
Example not_even_1001' : ~(even 1001).
Proof.
(* 课上已完成 *)
rewrite <- even_bool_prop.
unfold not.
simpl.
intro H.
discriminate H.
Qed.
相等性提供了另一个互补的例子,在命题的世界中它有时更容易处理。
在涉及 n 和 m 的证明中,知道 n =? m = true
通常没什么直接的帮助。然而如果我们将该语句转换为等价的 n = m 形式,
则可利用该等式改写证明目标。
Lemma plus_eqb_example : ∀ n m p : nat,
n =? m = true → n + p =? m + p = true.
Proof.
(* 课上已完成 *)
intros n m p H.
rewrite eqb_eq in H.
rewrite H.
rewrite eqb_eq.
reflexivity.
Qed.
我们不会在这里详细讨论互映技巧,然而对于展示布尔计算与一般命题的互补优势而言,
它是个很好的例子,在后面的章节中,能够在布尔和命题的世界之间来回穿梭通常会非常方便。
练习:2 星, standard (logical_connectives)
以下引理将本章中讨论的命题联结词与对应的布尔操作关联了起来。Lemma andb_true_iff : ∀ b1 b2:bool,
b1 && b2 = true ↔ b1 = true ∧ b2 = true.
Proof.
(* 请在此处解答 *) Admitted.
Lemma orb_true_iff : ∀ b1 b2,
b1 || b2 = true ↔ b1 = true ∨ b2 = true.
Proof.
(* 请在此处解答 *) Admitted.
☐
练习:3 星, standard (eqb_list)
给定一个用于测试类型为 A 的元素相等关系的布尔操作符 eqb, 我们可以定义函数 eqb_list 来测试元素类型为 A 的列表的相等关系。 请完成以下 eqb_list 函数的定义。要确定你的定义是否正确,请证明引理 eqb_list_true_iff。Fixpoint eqb_list {A : Type} (eqb : A → A → bool)
(l1 l2 : list A) : bool
(* 将本行替换成 ":= _你的_定义_ ." *). Admitted.
Lemma eqb_list_true_iff :
∀ A (eqb : A → A → bool),
(∀ a1 a2, eqb a1 a2 = true ↔ a1 = a2) →
∀ l1 l2, eqb_list eqb l1 l2 = true ↔ l1 = l2.
Proof.
(* 请在此处解答 *) Admitted.
☐
Fixpoint forallb {X : Type} (test : X → bool) (l : list X) : bool :=
match l with
| [] ⇒ true
| x :: l' ⇒ andb (test x) (forallb test l')
end.
请证明以下定理,它将 forallb 与之前的定义中 All 的性质联系了起来。
Theorem forallb_true_iff : ∀ X test (l : list X),
forallb test l = true ↔ All (fun x ⇒ test x = true) l.
Proof.
(* 请在此处解答 *) Admitted.
(未分级的思考题)函数 forallb 是否还存在尚未被此规范刻画到的重要性质?
(* 请在此处解答 *)
☐
经典逻辑 vs. 构造逻辑
为了在操作上理解为何如此, 回忆一下,在证明形如 P ∨ Q
的陈述时,我们使用了 left 与 right 策略,它们能够有效地知道析取的哪边成立。
然而在 excluded_middle 中,P 是被全称量化的'任意'命题,我们对它一无所知。
我们没有足够的信息来选择使用 left 或 right 中的哪一个。就像 Coq
因为缺乏信息而无法在函数内部机械地确定 P 是否成立一样。
然而,如果我们恰好知道 P 与某个布尔项互映,那么就能很轻易地知道它是否成立了:
我们只需检查 b 的值即可。
Theorem restricted_excluded_middle : ∀ P b,
(P ↔ b = true) → P ∨ ¬ P.
Proof.
intros P [] H.
- left. rewrite H. reflexivity.
- right. rewrite H. intros contra. discriminate contra.
Qed.
特别来说,对于自然数 n 和 m 的 n = m 而言,排中律是成立的。
Theorem restricted_excluded_middle_eq : ∀ (n m : nat),
n = m ∨ n ≠ m.
Proof.
intros n m.
apply (restricted_excluded_middle (n = m) (n =? m)).
symmetry.
apply eqb_eq.
Qed.
一般的排中律在 Coq 中默认并不可用,因为它是常见的逻辑系统(如
ZFC)中的标准特性。尽管如此,不假设排中律的成立仍有其独特的优点:
Coq 中的陈述可以构造出比标准数学中同样陈述更强的断言。特别是,
当存在 ∃ x, P x 的 Coq 证明时,我们可以直接给出一个使 P x
得证的值 x。换言之,任何关于存在性的证明必定是'构造性'的。
像 Coq 一样不假设排中律成立的逻辑系统被称作'构造逻辑'。
像 ZFC 这样更加传统的,排中律对于任何命题都成立的逻辑系统则被称作'经典逻辑'。
以下示例展示了为何假设排中律成立会导致非构造性证明:
'命题':存在无理数 a 和 b 使得 a ^ b(a 的 b 次方)为有理数。
'证明':易知 sqrt 2 为无理数。若 sqrt 2 ^ sqrt 2 为有理数,
那么可以取 a = b = sqrt 2 证明结束;否则 sqrt 2 ^ sqrt 2 为无理数。
此时,我们可以取 a = sqrt 2 ^ sqrt 2 和 b = sqrt 2,因为
a ^ b = sqrt 2 ^ (sqrt 2 × sqrt 2) = sqrt 2 ^ 2 = 2. ☐
看到发生什么了吗?我们使用排中律在不知道 sqrt 2 ^ sqrt 2
是否为有理数的情况下就分别考虑了这两种情况!因此,我们通过证明知道了这样的
a 和 b 存在,但却无法确切知道它们的值(至少无法从此论据中获知)。
即便构造逻辑很有用,它也有自身的限制:存在很多容易用经典逻辑证明的命题,
用构造证明只会更加复杂,而对于某些已知的命题而言这样的构造性证明甚至不存在!
幸运的是,排中律和函数外延性一样都是与 Coq 的逻辑系统兼容的,
我们可以安全地将它作为公理添加到 Coq 中。然而,在本书中我们不必如此:
我们所涉及的结构都可以完全用构造逻辑得到,所需的额外代价则微不足道。
我们需要一定的实践才能理解哪些证明技巧不应在构造推理中使用,
而其中的反证法尤为臭名昭著,因为它会导向非构造性证明。这里有个典型的例子:
假设我们想要证明存在 x 具有某种性质 P,即存在 P x。我们先假设结论为假,
也就是说 ¬ ∃ x, P x。根据此前提,不难推出 ∀ x, ¬ P x。
如果我们能够根据此中间事实得到矛盾,就能得到一个存在性证明而完全不必指出一个
x 的值使得 P x 成立!
从构造性的角度来看,这里存在着技术上的瑕疵,即我们试图通过对
¬ ¬ (∃ x, P x) 的证明来证明 ∃ x, P x。从以下练习中我们会看到,
允许自己从任意陈述中去掉双重否定等价于引入排中律。因此,只要我们不引入排中律,
就无法在 Coq 中编码此推理。
练习:3 星, standard (excluded_middle_irrefutable)
证明通用排中律公理与 Coq 的一致性需要复杂的推理,而且并不能在 Coq 自身中进行。然而,以下定理蕴含了假设可判定性公理(即排中律的一个特例) 成立对于任何'具体的'命题 P 而言总是安全的。之所以如此, 是因为我们无法证明这类公理的否定命题。假如我们可以的话,就会同时有 ¬ (P ∨ ¬P) 和 ¬ ¬ (P ∨ ¬P)(因为根据引理 double_neg,P 蕴含 ¬ ¬ P), 而这会产生矛盾。但因为我们不能,所以将 P ∨ ¬P 作为公理加入是安全的。练习:3 星, advanced (not_exists_dist)
在经典逻辑中有这样一条定理,它断言以下两条命题是等价的:¬(∃ x, ¬P x)
∀ x, P x
Theorem not_exists_dist :
excluded_middle →
∀ (X:Type) (P : X → Prop),
¬ (∃ x, ¬ P x) → (∀ x, P x).
Proof.
(* 请在此处解答 *) Admitted.
☐
练习:5 星, standard, optional (classical_axioms)
对于喜欢挑战的读者,以下练习来自于 Bertot 与 Casteran 所著的 Coq'Art 一书中第 123 页。以下四条陈述的每一条,加上 excluded_middle 可以认为刻画了经典逻辑。我们无法在 Coq 中证明其中的任意一条, 不过如果我们希望在经典逻辑下工作的话,可以安全地将其中任意一条作为公理添加到 Coq 中而不会造成不一致性。Definition peirce := ∀ P Q: Prop,
((P→Q)->P)->P.
Definition double_negation_elimination := ∀ P:Prop,
~~P → P.
Definition de_morgan_not_and_not := ∀ P Q:Prop,
~(~P ∧ ¬Q) → P∨Q.
Definition implies_to_or := ∀ P Q:Prop,
(P→Q) → (¬P∨Q).
(* 请在此处解答 *)
☐
(* 2022-03-14 05:26:56 (UTC+00) *)