【问题标题】:Writing well-founded programs in Coq using Fix or Program Fixpoint使用 Fix 或 Program Fixpoint 在 Coq 中编写有根据的程序
【发布时间】:2017-02-16 21:43:32
【问题描述】:

按照 Chlipala 书 GeneralRec 章节中给出的示例,我正在尝试编写归并排序算法。

这是我的代码

Require Import Nat.

Fixpoint insert (x:nat) (l: list nat) : list nat :=
  match l with
  | nil => x::nil
  | y::l' => if leb x y then
              x::l
            else
              y::(insert x l')
  end.

Fixpoint merge (l1 l2 : list nat) : list nat :=
  match l1 with
  | nil => l2
  | x::l1' => insert x (merge l1' l2)
  end.

Fixpoint split (l : list nat) : list nat * list nat :=
  match l with
  | nil => (nil,nil)
  | x::nil => (x::nil,nil)
  | x::y::l' =>
    let (ll,lr) := split l' in
    (x::ll,y::lr)
  end.

Definition lengthOrder (l1 l2 : list nat) :=
  length l1 < length l2.

Theorem lengthOrder_wf : well_founded lengthOrder.
Admitted.

问题是无法使用命令Fixpoint 编写mergeSort 函数,因为该函数在结构上没有递减:

Fixpoint mergeSort (l: list nat) : list nat :=
  if leb (length l) 1 then l
  else
    let (ll,lr) := split l in
    merge (mergeSort ll) (mergeSort lr).

相反,可以将命令Program FixpointDefinition 与术语Fix 一起使用(如Chlipala 书中所述)。

但是,如果我正在写这篇文章

Definition mergeSort : list nat -> list nat.
refine (Fix lengthOrder_wf (fun (l: list nat) => list nat)
      (fun (l : list nat) => (fun mergeSort : (forall ls : list nat, lengthOrder ls l -> list nat )=>
                           if leb (length l) 1 then
                             let (ll,lr) := split l in
                             merge (mergeSort ll _) (mergeSort lr _)
                           else
                             l))).

我正在实现不可能的目标:

2 subgoals, subgoal 1 (ID 65)

  l : list nat
  mergeSort : forall ls : list nat, lengthOrder ls l -> list nat
  ll, lr : list nat
  ============================
  lengthOrder ll l

subgoal 2 (ID 66) is:
 lengthOrder lr l

这就是为什么 Chlipala 建议以这种方式更改 mergeSort 的定义:

Definition mergeSort : list nat -> list nat.
  refine (Fix lengthOrder_wf (fun _ => list nat)
              (fun (ls : list nat)
                 (mergeSort : forall ls' : list nat, lengthOrder ls' ls -> list nat) =>
                 if Compare_dec.le_lt_dec 2 (length ls)
                 then let lss := split ls in
                      merge (mergeSort (fst lss) _) (mergeSort (snd lss) _)
                 else ls)).

产生以下目标:

2 subgoals, subgoal 1 (ID 68)

  ls : list nat
  mergeSort : forall ls' : list nat, lengthOrder ls' ls -> list nat
  l : 2 <= length ls
  lss := split ls : list nat * list nat
  ============================
  lengthOrder (fst lss) ls

subgoal 2 (ID 69) is:
 lengthOrder (snd lss) ls

这个新定义对我来说听起来很神奇。所以我想知道:

  • 从第一个定义开始,是否还可以证明函数的充分性?
  • 否则为什么第一个定义不起作用?
  • 基本用户如何轻松地从第一个定义转到第二个定义?

【问题讨论】:

  • length (fst (split l)) &lt; length l 是可证明的!观察你在上下文中有H : 2 &lt;= length l。您链接的 CPDT 章节中的 split_wf1 引理在这里非常有用:Lemma split_wf1 : forall ls, 2 &lt;= length ls -&gt; lengthOrder (fst (split ls)) ls

标签: coq


【解决方案1】:

很容易看出,您需要进行两项更改才能获得 A. Chlipala 的解决方案。

1) 在执行split 时,您需要记住lllr 来自split,否则它们将是一些任意列表,不可能比原始列表l 短。

以下代码未能保存此类信息:

let (ll,lr) := split l in
  merge (mergeSort ll _) (mergeSort lr _)

因此需要替换为

let lss := split ls in
  merge (mergeSort (fst lss) _) (mergeSort (snd lss) _)

它保留了我们需要的东西。

失败是由于 Coq 无法记住 lllr 来自 split l 而发生这种情况是因为 let (ll,lr) 只是伪装的 match(参见手册,§2.2.3)。

回想一下,模式匹配的目的是(松散地说)

  • 解压缩归纳数据类型的某些值的组件并将它们绑定到某些名称(我们将在答案的第二部分中需要它)和
  • 用相应模式匹配分支中的特殊情况替换原始定义。

现在,观察split l 在我们对其进行模式匹配之前不会出现在目标或上下文中的任何地方。我们只是随意将其引入定义中。这就是为什么模式匹配没有给我们任何东西——我们不能在目标或上下文中用它的“特殊情况”((ll,lr))替换split l,因为在任何地方都没有split l

还有另一种使用逻辑相等的方法 (=):

(let (ll, lr) as s return (s = split l -> list nat) := split l in
   fun split_eq => merge (mergeSort ll _) (mergeSort lr _)) eq_refl

这类似于使用remember 策略。我们已经摆脱了fstsnd,但这是一个巨大的矫枉过正,我不会推荐它。


2) 我们需要证明的另一件事是lllr2 &lt;= length l 时比l 短。

由于if-表达式也是伪装的match(它适用于具有完全两个构造函数的任何归纳数据类型),我们需要一些机制来记住leb 2 (length l) = truethen 分支。同样,由于我们在任何地方都没有leb,因此这些信息会丢失。

这个问题至少有两种可能的解决方案:

  • 我们要么将leb 2 (length l) 记住为一个等式(就像我们在第一部分中所做的那样),要么
  • 我们可以使用一些结果类型的比较函数,其行为类似于bool(因此它可以表示两种选择),但它还应该记住我们需要的一些额外信息。然后我们可以对比较结果进行模式匹配并提取信息,当然在这种情况下必须是2 &lt;= length l的证明。

我们需要一种类型,它能够在leb m n 返回true 的情况下携带m &lt;= n 的证明,并且在其他情况下携带m &gt; n 的证明。 标准库中有一种类型可以做到这一点!它叫sumbool

Inductive sumbool (A B : Prop) : Set :=
    left : A -> {A} + {B} | right : B -> {A} + {B}

{A} + {B} 只是sumbool A B 的符号(语法糖)。 就像bool 一样,它有两个构造函数,但此外它还记得AB 两个命题之一的证明。当您使用if 对其进行案例分析时,它相对于bool 的优势就会显现:您在then 分支中获得了A 的证明,在else 分支中获得了B 的证明。换句话说,您可以使用事先保存的上下文,而bool 不携带任何上下文(仅在程序员的脑海中)。

而我们正是需要这个!好吧,不在else 分支中,但我们希望在then 分支中获得2 &lt;= length l。所以,让我们问一下 Coq 是否已经有一个返回类型的比较函数:

Search (_ -> _ -> {_ <= _} + {_}).

(*
output:
le_lt_dec: forall n m : nat, {n <= m} + {m < n}
le_le_S_dec: forall n m : nat, {n <= m} + {S m <= n}
le_ge_dec: forall n m : nat, {n <= m} + {n >= m}
le_gt_dec: forall n m : nat, {n <= m} + {n > m}
le_dec: forall n m : nat, {n <= m} + {~ n <= m}
*)

五个结果中的任何一个都可以,因为我们只需要在一种情况下进行证明。

因此,我们可以将if leb 2 (length l) then ... 替换为if le_lt_dec 2 (length l) ...,并在证明上下文中获得2 &lt;= length,这样我们就可以完成证明了。

【讨论】:

  • 感谢您的回答。关键是 Coq 需要的所有假设都应该反映在表达式的类型中,因此,我们需要依赖引理/构造来实现这一点。最后,这很简单!
  • 是的。与普通函数式编程相比,我们通常必须更明确地了解信息流。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-14
  • 2012-10-01
相关资源
最近更新 更多