让我教你一个奇怪的技巧。它可能无法解决您所有的担忧,但它可能会有所帮助,至少在概念上是这样。
让我们实现自然数的加法,后者由下式给出
Inductive nat : Set :=
| zero : nat
| suc : nat -> nat.
您可以尝试通过策略编写加法,但是会发生这种情况。
Theorem plus' : nat -> nat -> nat.
Proof.
induction 1.
plus' < 2 subgoals
============================
nat -> nat
subgoal 2 is:
nat -> nat
你看不到你在做什么。
诀窍是更仔细地观察你在做什么。我们可以引入一个无偿依赖的类型,克隆nat
。
Inductive PLUS (x y : nat) : Set :=
| defPLUS : nat -> PLUS x y.
这个想法是PLUS x y
“计算方式plus x y
”的类型。我们需要一个投影,允许我们提取这种计算的结果。
Theorem usePLUS : forall x y, PLUS x y -> nat.
Proof.
induction 1.
exact n.
Defined.
现在我们准备通过证明来编程。
Theorem mkPLUS : forall x y, PLUS x y.
Proof.
mkPLUS < 1 subgoal
============================
forall x y : nat, PLUS x y
目标的结论向我们展示了我们当前的左侧和上下文。C-c C-c
在 Agda 中的类似物是...
induction x.
mkPLUS < 2 subgoals
============================
forall y : nat, PLUS zero y
subgoal 2 is:
forall y : nat, PLUS (suc x) y
你可以看到它已经完成了案例拆分!让我们打破基本情况。
intros y.
exact (defPLUS zero y y).
调用 PLUS 的构造函数就像写一个方程。=
在第三个参数之前想象一个符号。对于 step 情况,我们需要进行递归调用。
intros y.
eapply (fun h => (defPLUS (suc x) y (suc (usePLUS x y h)))).
为了进行递归调用,我们usePLUS
使用我们想要的参数进行调用,这里x
和y
,但是我们抽象了第三个参数,这是对如何实际计算它的解释。我们只剩下那个子目标,实际上是终止检查。
mkPLUS < 1 subgoal
x : nat
IHx : forall y : nat, PLUS x y
y : nat
============================
PLUS x y
现在,您不再使用 Coq 的防护检查,而是使用...
auto.
...它检查归纳假设是否涵盖递归调用。是
Defined.
我们有一个工人,但我们需要一个包装器。
Theorem plus : nat -> nat -> nat.
Proof.
intros x y.
exact (usePLUS x y (mkPLUS x y)).
Defined.
我们准备好了。
Eval compute in (plus (suc (suc zero)) (suc (suc zero))).
Coq < = suc (suc (suc (suc zero)))
: nat
您有一个交互式构建工具。您可以通过使类型更具信息性来向您展示您正在解决的问题的相关细节。生成的证明脚本...
Theorem mkPLUS : forall x y, PLUS x y.
Proof.
induction x.
intros y.
exact (defPLUS zero y y).
intros y.
eapply (fun h => (defPLUS (suc x) y (suc (usePLUS x y h)))).
auto.
Defined.
...明确说明它构建的程序。你可以看到这是定义加法。
如果你自动化这个程序构建设置,然后在一个界面上显示你正在构建的程序和关键问题简化策略,你会得到一个有趣的小编程语言,叫做 Epigram 1。