我想知道哪个是在Coq中定义部分递归函数的最佳方法。
假设我想定义一个返回自然数列表的最大元素的函数。但是,我们希望仅为非空列表定义此函数。
我一直在尝试以下方法:
Fixpoint MaxValueList (l : list nat | l <> []) : nat :=
match l with
|[n] => n
|n::l' => (max n (MaxValueList l'))
end.
但是,这不起作用,因为::
是列表的构造函数而不是{l : list nat | l <> []}
。
我的其他尝试是使用option
。在这种情况下,我尝试了以下内容:
Fixpoint MaxValueList (l : list nat | l <> []) : option nat :=
match l with
|[] => None
|[n] => n
|n::l' => (max n (MaxValueList l'))
end.
自max : nat -> nat -> nat
和MaxValueList l' : option nat
以来,这无效。
答案 0 :(得分:3)
以下是您问题的可能解决方案:
Require Import Coq.Lists.List.
Import ListNotations.
Definition MaxValueListAux (n : nat) (l : list nat) : nat :=
fold_left max l n.
Definition MaxValueListNE (l : list nat) : l <> [] -> nat :=
match l with
| [] => fun H => match H eq_refl with end
| n :: l' => fun _ => MaxValueListAux n l'
end.
在这里,我将原始的MaxValueList
拆分为两部分:一个MaxValueListAux
函数,用于计算给定默认值的列表的最大元素; MaxValueListNE
,它是一个包装到第一个函数并获取证明参数。第二个函数只是放弃了不可能的情况,并用适当的参数调用第一个函数;我将在短期内解释这是如何运作的。由于这种分裂,我们不会遇到在MaxValueListNE
的非空分支中构造证明参数的问题;我们要做的唯一证据就是摆脱空案。
请注意,第二个函数是以一种奇怪的方式编写的:我没有将l <> []
声明为MaxValueListNE
的另一个参数,而是将它放在该函数的返回类型中。这是因为Coq中依赖模式匹配的方式;粗略地说,每当您需要将在match
上获得的信息(例如l
在[]
分支上为空的事实)与来自“外部”的信息进行组合时匹配(例如l <> []
的证明),您需要使match
语句返回一个函数。这导致Adam Chlipala称之为护送模式的技巧,您可以了解更多关于here的信息。将该参数作为返回类型的一部分,允许Coq推断match
语句所需的类型注释。
那么,MaxValueListNE
究竟是如何运作的呢?为了理解这一点,我们必须谈谈模式匹配如何在Coq中工作。正如我之前提到的,我们以这种特殊方式编写了这个函数,以便Coq可以推断出一些缺少类型的注释。但我们也可以手工添加:
Definition MaxValueListNE (l : list nat) : l <> [] -> nat :=
match l return l <> [] -> nat with
| [] => fun (H : [] <> []) => match H eq_refl with end
| n :: l' => fun (_ : n :: l' <> []) => MaxValueListAux n l'
end.
当Coq读取此定义时,它会尝试键入检查函数,特别是确保match
的每个分支都返回它承诺返回的类型的元素。但是当这样做时,允许用对应于该分支的任何值替换每个出现的判别符(在这种情况下,l
)。在上面的第一个分支中,这意味着将l
替换为[]
,这反过来意味着返回的函数采用类型[] <> []
的参数。回想一下,在Coq中,[] <> []
与[] = [] -> False
是一回事。由于False
没有构造函数,我们可以通过H eq_refl
上的模式匹配摆脱那个矛盾的分支,其中eq_refl
是相等类型的唯一构造函数,并且被认为具有类型在这种特殊情况下[] = []
。
现在,值得注意的是,添加更多类型信息并不一定是好的。对于您的函数,我更喜欢省略proof参数并简单地编写Definition MaxValueList l := fold_left max l 0
。请注意,0是max
的中性元素,因此在空案例上返回该值是有意义的。例如,它允许我们证明像
forall l1 l2, MaxValueList (l1 ++ l2) = max (MaxValueList l1) (MaxValueList l2)
当然,这并不适用于所有情况:如果我们将max
替换为min
,则上述定理将不再适用。尽管如此,我认为编程和推理适用于任意列表的MinValueList
函数仍然更容易:如果某个关于该函数的结果仅适用于非空元素,我们可以将这些假设添加到我们的定理中: / p>
forall l1 l2, l1 <> [] ->
MinValueList (l1 ++ l2) = min (MinValueList l1) (MinValueList l2).
这就是人们通常在Coq中定义分裂的方式。我们不是使用部分函数div : forall (n1 n2 : nat), n2 <> 0 -> nat
,而是编写一个总函数div : nat -> nat -> nat
,并证明定理关于该函数,假设它的第二个参数不为零。
答案 1 :(得分:1)
另一种方法是使用refine
策略来构造函数,使用一个证明样式的定理。
每当我需要用证明构建术语(比如你的函数)时,我更喜欢使用定义并改进策略,因为它提供了一种更清晰,更简单的方法来自动化关于命题的证明。
下面是我在简单形式化中定义的类似函数。您可以轻松修改它以强制执行非空列表输入要求。我们的想法是使用精炼策略来提供函数的结构,并且函数属性的证明项用孔标记&#34; _&#34;后者充满了战术。
Definition max_list : forall (l : list nat), {n | forall n', In n' l -> n > n'}.
refine (fix max_list (l : list nat) : {n | forall n', In n' l -> n > n'} :=
match l with
| nil => exist _ 0 _
| x :: l' =>
match max_list l' with
exist x' _ =>
match le_gt_dec x x' with
| left _ => exist _ (S x') _
| right _ => exist _ (S x) _
end
end
end) ; clear max_list ; simpl in * ; intuition ;
try (match goal with
| [H : context[In _ _ -> _],
H1 : In _ _ |- _] => apply H in H1 ; try omega
end).
Defined.
有一种所谓的Program功能,可以简化用依赖类型编写函数。也许值得检查一下。我的经验是它产生了一些复杂的假设,因此我更喜欢使用refine
。