ProofObjects柯里-霍华德对应
"'算法是证明的计算性内容。'" --Robert Harper
Print ev.
(* ==>
Inductive ev : nat -> Prop :=
| ev_0 : ev 0
| ev_SS : forall n, ev n -> ev (S (S n)).
*)
我们可以换种方式来解读“:”:用“是……的证明”而非“具有……类型”。例如将 ev
定义中第二行的 ev_0 : ev 0 读作“ev_0 是 ev 0 的证明”而非“ev_0 的类型为
ev 0”。
此处 : 既在类型层面表达“具有……类型”,又在命题层面表示“是……的证明”。
这种双关称为'柯里-霍华德同构(Curry-Howard correspondence)'。
它指出了逻辑与计算之间的深层联系:
该同构启发很多看问题的新方法。首先,对 ev_SS 构造子的理解变得更加自然:
命题 ~ 类型 证明 ~ 数据值[Wadler 2015] 里有简单的历史和最新的详细介绍可供参考。
可以将其读作“ev_SS 构造子接受两个参数——数字 n 以及命题 ev n
的证明——并产生 ev (S (S n)) 的证明。”
现在让我们回顾一下之前有关 ev 的一个证明。
就像是处理普通的数据值和函数一样,我们可以使用 Print 指令来查看
这个证明脚本所产生的'证据对象 (proof object)'
实际上,我们也可以不借助脚本'直接'写出表达式作为证明。
表达式 ev_SS 2 (ev_SS 0 ev_0) 可视为向构造子 ev_SS 传入参数 2 和 0
等参数,以及对应的 ev 2 与 ev 0 之依据所构造的证明。或言之,视 ev_SS
为“构造证明”之原语,需要给定一个数字,并进一步提供该数为偶数之依据以构造证明。
其类型表明了它的功能:
∀ n, ev n → ev (S (S n)),
类似地,多态类型 ∀ X, list X 表明可以将 nil
视为从某类型到由该类型元素组成的空列表的函数。
我们在 Logic 这一章中已经了解到,我们可以使用函数应用
的语法来实例化引理中的全称量化变量,也可以使用该语法提供引理所要求
的假设。例如:
∀ n, ev n → ev (S (S n)),
证明脚本
Theorem ev_4'' : ev 4.
Proof.
Show Proof.
apply ev_SS.
Show Proof.
apply ev_SS.
Show Proof.
apply ev_0.
Show Proof.
Qed.
在任意的给定时刻,Coq已经构造了一个包含一个“洞(hole)”(即
?Goal )的项,并且Coq知道该洞需要什么类型的证据来填补。
每一个洞对应一个子目标。当没有子目标时,代表证明已经完成。此时,我
们构造的证明将会被存储在全局环境中,其名字就是在 Theorem 中给定的名字
策略证明非常有用且方便,但是它们并不是必要的:原则上,我们总是能够
手动构造想要的证据,如下所示。此处我们可以通过 Definition (而非
Theorem)来直接给这个证据一个全局名称。
所有这些构造证明的不同方式,对应的存储在全局环境中的证明是完全一样的。
Print ev_4.
(* ===> ev_4 = ev_SS 2 (ev_SS 0 ev_0) : ev 4 *)
Print ev_4'.
(* ===> ev_4' = ev_SS 2 (ev_SS 0 ev_0) : ev 4 *)
Print ev_4''.
(* ===> ev_4'' = ev_SS 2 (ev_SS 0 ev_0) : ev 4 *)
Print ev_4'''.
(* ===> ev_4''' = ev_SS 2 (ev_SS 0 ev_0) : ev 4 *)
Theorem ev_8 : ev 8.
Proof.
(* 请在此处解答 *) Admitted.
Definition ev_8' : ev 8
(* 将本行替换成 ":= _你的_定义_ ." *). Admitted.
☐
量词,蕴含式,函数
Theorem ev_plus4 : ∀ n, ev n → ev (4 + n).
Proof.
intros n H. simpl.
apply ev_SS.
apply ev_SS.
apply H.
Qed.
对应 ev_plus4 的证明对象是什么?
我们在寻找一个'类型(Type)'是 ∀ n, ev n → ev (4 + n) 的表达式——也
就是说,一个接受两个参数(一个数字和一个证据)并返回一个证据的
'函数(Function)'!
它的证据对象:
Definition ev_plus4' : ∀ n, ev n → ev (4 + n) :=
fun (n : nat) ⇒ fun (H : ev n) ⇒
ev_SS (S (S n)) (ev_SS n H).
回顾 fun n ⇒ blah 意味着“一个函数,给定 n,产生 blah”,
并且Coq认为 4 + n 和 S (S (S (S n))) 是同义词,所以另一种写出
这个定义的方式是:
Definition ev_plus4'' (n : nat) (H : ev n)
: ev (4 + n) :=
ev_SS (S (S n)) (ev_SS n H).
Check ev_plus4''
: ∀ n : nat,
ev n →
ev (4 + n).
当我们将 ev_plus4 证明的命题视为一个函数类型时,我们可以发现一个
有趣的现象:第二个参数的类型,ev n,依赖于第一个参数 n 的'值'。
虽然这样的'依赖类型 (Dependent type)'在传统的编程语言中并不存在,
但是它们对于编程来说有时候非常有用。最近它们在函数式编程社区里的活
跃很好地表明了这一点。
注意到蕴含式(→)和量化(∀)都表示证据上的函数。事实上,他们
是同一个东西:当我们使用∀时没有依赖,就可以简写为当→。即,我
们没有必要给与箭头左边的类型一个名字:
∀ (x:nat), nat
= ∀ (_:nat), nat
= nat → nat
例如,考虑下列命题:
∀ (x:nat), nat
= ∀ (_:nat), nat
= nat → nat
这个命题的一个证明项会是一个拥有两个参数的函数:一个数字n
和一个表明n是偶数的证据E。但是对于这个证据来说,名字E并没有
在ev_plus2剩余的陈述里面被使用,所以还专门为它取一个名字并没有意
义。因此我们可以使用虚拟标志符_来替换真实的名字:
或者,等同地,我们可以使用更加熟悉的记号:
总的来说,"P → Q"只是 "∀ (_:P), Q"的语法糖。
Definition add1 : nat → nat.
intro n.
Show Proof.
apply S.
Show Proof.
apply n. Defined.
Print add1.
(* ==>
add1 = fun n : nat => S n
: nat -> nat
*)
Compute add1 2.
(* ==> 3 : nat *)
注意到我们通过使用.终止了Definition,而不是使用:=和一个项来
定义它。这个记号会告诉Coq进入'证明脚本模式(Proof Scripting
Mode)'来构造类型是nat → nat的项。并且,我们通过使用Defined而不是
Qed来终止证明;这使得这个定义是'透明的(Transparent)',所以它可
以在计算中就像正常定义的函数一样被使用。(通过Qed定义的对象在计
算中是不透明的。)
这个特性主要是在定义拥有依赖类型的函数时非常有用。我们不会在本书中
详细讨论后者。但是它确实表明了Coq里面基本思想的一致性和正交性。
逻辑联结词作为归纳类型
这个定义能够解释为什么destruct和intros模式能用于一个合取前提。
情况分析允许我们考虑所有P ∧ Q可能被证明的方式——只有一种方式(即
conj构造子)。
类似地,split策略能够用于所有只有一个构造子的归
纳定义命题。特别地,它能够用于and:
Lemma and_comm : ∀ P Q : Prop, P ∧ Q ↔ Q ∧ P.
Proof.
intros P Q. split.
- intros [HP HQ]. split.
+ apply HQ.
+ apply HP.
- intros [HQ HP]. split.
+ apply HP.
+ apply HQ.
Qed.
这解释了为什么一直以来我们能够使用策略来操作and的归纳定义。我们
也可以使用模式匹配来用它直接构造证明。例如:
Definition and_comm'_aux P Q (H : P ∧ Q) : Q ∧ P :=
match H with
| conj HP HQ ⇒ conj HQ HP
end.
Definition and_comm' P Q : P ∧ Q ↔ Q ∧ P :=
conj (and_comm'_aux P Q) (and_comm'_aux Q P).
Module Or.
Inductive or (P Q : Prop) : Prop :=
| or_introl : P → or P Q
| or_intror : Q → or P Q.
End Or.
这个声明解释了destruct策略在一个析取前提上的行为,产生的子类型和
or_introl以及or_intror构造子的形状相匹配。
又一次地,我们可以不使用策略,直接写出涉及or的定义的证明对象。
练习:2 星, standard, optional (or_commut'')
尝试写下or_commut的显式证明对象。(不要使用Print来偷看我们已经 定义的版本!)Module Ex.
Inductive ex {A : Type} (P : A → Prop) : Prop :=
| ex_intro : ∀ x : A, P x → ex P.
End Ex.
打包之后的命题可以通过解包操作受益。这里的核心定义是为了用于构造
ex P命题的类型构造器ex,其中P自身是一个从类型为A的证据类型
值到命题的'函数(Function)'。构造子ex_intro提供了一个给定
证据类型x和P x的证 明,可以构造ex P的证据的方式。
我们更加熟悉的类型∃ x, P x可以转换为一个涉及ex的表达式:
下面是我们如何定义一个涉及ex的显式证明对象:
它拥有一个构造子(因此True的所有证据都是一样的,所以给出一个
True的证明并没有信息量。)
False也一样的简单——事实上,它是如此简单,以致于第一眼看上去像是一个
语法错误。
也就是说, False是一个'没有'构造子的归纳类型--即,没有任何方式能
够构造一个它的证明。
Module MyEquality.
Inductive eq {X:Type} : X → X → Prop :=
| eq_refl : ∀ x, eq x x.
Notation "x == y" := (eq x y)
(at level 70, no associativity)
: type_scope.
我们可以这样理解这个定义,给定一个集合X,它定义了由X的一对值
(x和y)所索引的“x与y相等”的一'系列(Family)'的命题。只有
一种方式能够构造该系列中成员的证据:将构造子eq_refl应用到类型X
和值x:X,产生一个x等于x的证据。
其它形如 eq x y 的类型中的 x 和 y 并不相同,因此是非居留的。
我们可以使用eq_refl来构造证据,比如说,2 = 2。那么我们能否使用
它来构造证据1 + 1 = 2呢?答案是肯定的。事实上,它就是同一个证据!
原因是如果两个项能够通过一些简单的计算规则'可转换(convertible)' ,
那么Coq认为两者“相等”。
这些计算规则,与Compute所使用的规则相似,包括函数应用的计算,定
义的内联,match语句的化简。
至今为止我们所用来证据相等关系的reflexivity策略本质上只是apply
eq_refl的简写。
在基于策略的相等关系证明中,转换规则通常隐藏在simpl的使用后面(在
其他策略中或显式或隐式,例如reflexivity)。
而在如下的显式证明对象中,你可以直接看到它们:
Definition four' : 2 + 2 == 1 + 3 :=
eq_refl 4.
Definition singleton : ∀ (X:Type) (x:X), []++[x] == x::[] :=
fun (X:Type) (x:X) ⇒ eq_refl [x].
练习:2 星, standard (equality__leibniz_equality)
相等关系的归纳定义隐含了'Leibniz相等关系(Leibniz equality)':当我们 说“x和y相等的时候”,我们意味着所有x满足的性质P,对于y 来说也满足。Lemma equality__leibniz_equality : ∀ (X : Type) (x y: X),
x == y → ∀ P:X→Prop, P x → P y.
Proof.
(* 请在此处解答 *) Admitted.
☐
练习:5 星, standard, optional (leibniz_equality__equality)
请说明,事实上,相等关系的归纳定义和Leibniz相等关系是 '等价的(equivalent)'。Lemma leibniz_equality__equality : ∀ (X : Type) (x y: X),
(∀ P:X→Prop, P x → P y) → x == y.
Proof.
(* 请在此处解答 *) Admitted.
☐
再论反演
- 接受一个前提H,该前提的类型P是通过归纳定义的,以及
- 对于P的定义里的每一个构造子C,
- 产生一个新的子目标,在该子目标中我们假设H是通过C构造的,
- 作为额外的假设,在子目标的上下文中增加C的论据(前提),
- 将C的结论(结果类型)与当前的目标相匹配,计算出为了能够应用C而必须成立的一些相等关系,
- 将这些相等关系加入上下文中(以及,为了方便,在目标中替换它们),以及
- 如果这些相等关系无法满足(例如,它们涉及到S n = O),那么立即解决这个子目标。
- 产生一个新的子目标,在该子目标中我们假设H是通过C构造的,
(* 2022-03-14 05:26:57 (UTC+00) *)