Coq Program Fixpoint vs 方程式是获得归约引理的最佳方法吗?
Coq Program Fixpoint vs equations as far as best way to get reduction lemmas?
我试图证明如何计算两个字符串之间的编辑距离的特定实现是正确的并产生相同的结果。我采用最自然的方式将编辑距离递归定义为单个函数(见下文)。这导致 coq 抱怨它无法确定递减参数。经过一番搜索,似乎使用 Program Fixpoint 机制并提供测量功能是解决此问题的一种方法。然而,这导致了下一个问题,即策略 simpl 不再按预期工作。我发现这个 有类似的问题,但我被卡住了,因为我不明白 Fix_sub 函数在 coq 为我的编辑距离函数生成的代码中扮演的角色,看起来更多比上一个问题中的简单示例复杂。
问题:
- 对于像编辑距离这样的功能,Equations 包是否比 Program Fixpoint(自动获取归约引理)更容易使用?上一个关于这方面的问题是从 2016 年开始的,所以我很好奇这方面的最佳实践是否从那时起有所发展。
- 我发现这个 coq program 涉及 edit_distance,它使用归纳定义的 prop 而不是函数。也许这就是我仍在努力思考 Curry-Howard 通信,但为什么 Coq 愿意接受 edit_distance 的归纳命题定义而没有 termination/measure 投诉,而不是函数驱动方法?这是否意味着有一个使用创造性定义的归纳类型的角度可以传递给 edit_distance,它包含成对包装的字符串和一个数字,并且 coq 上的过程更容易被接受为结构递归?
有没有更简单的方法使用程序固定点来减少?
Fixpoint min_helper (best :nat) (l : list nat) : nat :=
match l with
| nil => best
| h::t => if h<?best then min_helper h t else min_helper best t
end.
Program Fixpoint edit_distance (s1 s2 : string) {measure (length s1+ length s2)} : nat :=
match s1, s2 with
| EmptyString , EmptyString => O
| String char rest , EmptyString => length s1
| EmptyString , String char rest => length s2
| String char1 rest1 , String char2 rest2 =>
let choices : list nat := S ( edit_distance rest1 s2) :: S (edit_distance s1 rest2) :: nil in
if (Ascii.eqb char1 char2)
then min_helper (edit_distance rest1 rest2 ) choices
else min_helper (S (edit_distance rest1 rest2)) choices
end.
Next Obligation.
intros. simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. reflexivity. Qed.
Next Obligation.
simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. apply PeanoNat.Nat.le_succ_diag_r. Qed.
Next Obligation.
simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. apply PeanoNat.Nat.le_succ_diag_r. Qed.
Theorem simpl_edit : forall (s1 s2: string), edit_distance s1 s2 = match s1, s2 with
| EmptyString , EmptyString => O
| String char rest , EmptyString => length s1
| EmptyString , String char rest => length s2
| String char1 rest1 , String char2 rest2 =>
let choices : list nat := S ( edit_distance rest1 s2) :: S (edit_distance s1 rest2) :: nil in
if (Ascii.eqb char1 char2)
then min_helper (edit_distance rest1 rest2 ) choices
else min_helper (S (edit_distance rest1 rest2)) choices
end.
Proof. intros. induction s1.
- induction s2.
-- reflexivity.
-- reflexivity.
- induction s2.
-- reflexivity.
-- remember (a =? a0)%char as test. destruct test.
--- (*Stuck??? Normally I would unfold edit_distance but the definition coq creates after unfold edit_distance ; unfold edit_distance_func is hard for me to reason about*)
这种基于两个参数的递归有一个常见的技巧,即编写两个嵌套函数,每个函数都对两个参数之一进行递归。
这也可以从动态规划的角度来理解,编辑距离是通过遍历矩阵来计算的。更一般地,编辑距离函数 edit xs ys
可以被视为 nat
的矩阵,其中行由 xs
索引,列由 ys
索引。外部递归遍历 xs
行,对于其中的每一行,当 xs = x :: xs'
时,内部递归遍历其列 ys
以从另一行生成该行的条目较小的索引 xs'
.
From Coq Require Import String Ascii.
Local Open Scope string_scope.
Infix "::" := String.
(* structural recursion on xs *)
Fixpoint edit (xs ys : string) : nat :=
match xs with
| "" => String.length ys
| x :: xs' =>
(* structural recursion on ys *)
let fix edit_xs ys :=
match ys with
| "" => String.length xs
| y :: ys' =>
let orth := min (edit xs' ys) (edit_xs ys') in
if (x =? y)%char then
min (edit xs' ys') orth
else
orth
end in
edit_xs ys
end.
您可以改用 Function
,它随 Coq 一起提供,并为您生成一个归约引理(这实际上也会生成一个图,如 Inductive R_edit_distance
您提到的替代开发的脉络,但这里很粗糙——这可能是因为我为了简洁而进行的编辑)
Require Import String.
Require Import List.
Require Import PeanoNat.
Import ListNotations.
Require Import FunInd.
Require Recdef.
Fixpoint min_helper (best : nat) (l : list nat) : nat :=
match l with
| [] => best
| h :: t => if h <? best then min_helper h t else min_helper best t
end.
Function edit_distance
(ss : string * string) (* unfortunately, Function only supports one decreasing argument *)
{measure (fun '(s1, s2) => String.length s1 + String.length s2) ss} : nat :=
match ss with
| (String char1 rest1 as s1, String char2 rest2 as s2) =>
let choices := [S (edit_distance (rest1, s2)); S (edit_distance (s1, rest2))] in
if Ascii.eqb char1 char2
then min_helper (edit_distance (rest1, rest2)) choices
else min_helper (S (edit_distance (rest1, rest2))) choices
| (EmptyString, s) | (s, EmptyString) => String.length s
end.
all: intros; simpl; rewrite Nat.add_succ_r; repeat constructor.
Qed.
Check edit_distance_equation. (* : forall ss : string * string, edit_distance ss = ... *)
Print R_edit_distance. (* Inductive R_edit_distance : string * string -> nat -> Prop := ... *)
图表 Inductive
定义(无论是您引用的漂亮定义还是此处生成的混乱定义)不需要终止保证的原因是 Inductive
类型的项必须是有限的已经。项R_edit_distance ss n
(表示edit_distance ss = n
)应被视为edit_distance
计算步骤的记录或日志。虽然一般递归函数可能会陷入无限计算,但相应的 Inductive
类型排除了无限对数:如果 edit_distance ss
发散,R_edit_distance ss n
将完全无人居住 n
,所以没有爆炸。反过来,给定 ss
,edit_distance ss
是什么或 {n | R_edit_distance ss n}
中的术语,您实际上没有能力 计算 ,直到您完成一些终止证明。 (例如,证明 forall ss, {n | R_edit_distance ss n}
是 edit_distance
的一种终止证明形式。)
您在某些辅助类型上使用结构递归的想法是完全正确的(这是 唯一的 形式的递归,无论如何都可用;Program
和 Function
只是建立在它的基础上),但它与归纳图没有任何关系......
Fixpoint edit_distance
(s1 s2 : string) (n : nat) (prf : n = String.length s1 + String.length s2)
{struct n}
: nat := _.
按照上面的思路应该可以,但会很混乱。 (您 可以 递归图归纳的实例而不是 nat
这里,但是,同样,这只是开始构建图归纳的实例。)
我试图证明如何计算两个字符串之间的编辑距离的特定实现是正确的并产生相同的结果。我采用最自然的方式将编辑距离递归定义为单个函数(见下文)。这导致 coq 抱怨它无法确定递减参数。经过一番搜索,似乎使用 Program Fixpoint 机制并提供测量功能是解决此问题的一种方法。然而,这导致了下一个问题,即策略 simpl 不再按预期工作。我发现这个
问题:
- 对于像编辑距离这样的功能,Equations 包是否比 Program Fixpoint(自动获取归约引理)更容易使用?上一个关于这方面的问题是从 2016 年开始的,所以我很好奇这方面的最佳实践是否从那时起有所发展。
- 我发现这个 coq program 涉及 edit_distance,它使用归纳定义的 prop 而不是函数。也许这就是我仍在努力思考 Curry-Howard 通信,但为什么 Coq 愿意接受 edit_distance 的归纳命题定义而没有 termination/measure 投诉,而不是函数驱动方法?这是否意味着有一个使用创造性定义的归纳类型的角度可以传递给 edit_distance,它包含成对包装的字符串和一个数字,并且 coq 上的过程更容易被接受为结构递归?
有没有更简单的方法使用程序固定点来减少?
Fixpoint min_helper (best :nat) (l : list nat) : nat :=
match l with
| nil => best
| h::t => if h<?best then min_helper h t else min_helper best t
end.
Program Fixpoint edit_distance (s1 s2 : string) {measure (length s1+ length s2)} : nat :=
match s1, s2 with
| EmptyString , EmptyString => O
| String char rest , EmptyString => length s1
| EmptyString , String char rest => length s2
| String char1 rest1 , String char2 rest2 =>
let choices : list nat := S ( edit_distance rest1 s2) :: S (edit_distance s1 rest2) :: nil in
if (Ascii.eqb char1 char2)
then min_helper (edit_distance rest1 rest2 ) choices
else min_helper (S (edit_distance rest1 rest2)) choices
end.
Next Obligation.
intros. simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. reflexivity. Qed.
Next Obligation.
simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. apply PeanoNat.Nat.le_succ_diag_r. Qed.
Next Obligation.
simpl. rewrite <- plus_n_Sm. apply Lt.le_lt_n_Sm. apply PeanoNat.Nat.le_succ_diag_r. Qed.
Theorem simpl_edit : forall (s1 s2: string), edit_distance s1 s2 = match s1, s2 with
| EmptyString , EmptyString => O
| String char rest , EmptyString => length s1
| EmptyString , String char rest => length s2
| String char1 rest1 , String char2 rest2 =>
let choices : list nat := S ( edit_distance rest1 s2) :: S (edit_distance s1 rest2) :: nil in
if (Ascii.eqb char1 char2)
then min_helper (edit_distance rest1 rest2 ) choices
else min_helper (S (edit_distance rest1 rest2)) choices
end.
Proof. intros. induction s1.
- induction s2.
-- reflexivity.
-- reflexivity.
- induction s2.
-- reflexivity.
-- remember (a =? a0)%char as test. destruct test.
--- (*Stuck??? Normally I would unfold edit_distance but the definition coq creates after unfold edit_distance ; unfold edit_distance_func is hard for me to reason about*)
这种基于两个参数的递归有一个常见的技巧,即编写两个嵌套函数,每个函数都对两个参数之一进行递归。
这也可以从动态规划的角度来理解,编辑距离是通过遍历矩阵来计算的。更一般地,编辑距离函数 edit xs ys
可以被视为 nat
的矩阵,其中行由 xs
索引,列由 ys
索引。外部递归遍历 xs
行,对于其中的每一行,当 xs = x :: xs'
时,内部递归遍历其列 ys
以从另一行生成该行的条目较小的索引 xs'
.
From Coq Require Import String Ascii.
Local Open Scope string_scope.
Infix "::" := String.
(* structural recursion on xs *)
Fixpoint edit (xs ys : string) : nat :=
match xs with
| "" => String.length ys
| x :: xs' =>
(* structural recursion on ys *)
let fix edit_xs ys :=
match ys with
| "" => String.length xs
| y :: ys' =>
let orth := min (edit xs' ys) (edit_xs ys') in
if (x =? y)%char then
min (edit xs' ys') orth
else
orth
end in
edit_xs ys
end.
您可以改用 Function
,它随 Coq 一起提供,并为您生成一个归约引理(这实际上也会生成一个图,如 Inductive R_edit_distance
您提到的替代开发的脉络,但这里很粗糙——这可能是因为我为了简洁而进行的编辑)
Require Import String.
Require Import List.
Require Import PeanoNat.
Import ListNotations.
Require Import FunInd.
Require Recdef.
Fixpoint min_helper (best : nat) (l : list nat) : nat :=
match l with
| [] => best
| h :: t => if h <? best then min_helper h t else min_helper best t
end.
Function edit_distance
(ss : string * string) (* unfortunately, Function only supports one decreasing argument *)
{measure (fun '(s1, s2) => String.length s1 + String.length s2) ss} : nat :=
match ss with
| (String char1 rest1 as s1, String char2 rest2 as s2) =>
let choices := [S (edit_distance (rest1, s2)); S (edit_distance (s1, rest2))] in
if Ascii.eqb char1 char2
then min_helper (edit_distance (rest1, rest2)) choices
else min_helper (S (edit_distance (rest1, rest2))) choices
| (EmptyString, s) | (s, EmptyString) => String.length s
end.
all: intros; simpl; rewrite Nat.add_succ_r; repeat constructor.
Qed.
Check edit_distance_equation. (* : forall ss : string * string, edit_distance ss = ... *)
Print R_edit_distance. (* Inductive R_edit_distance : string * string -> nat -> Prop := ... *)
图表 Inductive
定义(无论是您引用的漂亮定义还是此处生成的混乱定义)不需要终止保证的原因是 Inductive
类型的项必须是有限的已经。项R_edit_distance ss n
(表示edit_distance ss = n
)应被视为edit_distance
计算步骤的记录或日志。虽然一般递归函数可能会陷入无限计算,但相应的 Inductive
类型排除了无限对数:如果 edit_distance ss
发散,R_edit_distance ss n
将完全无人居住 n
,所以没有爆炸。反过来,给定 ss
,edit_distance ss
是什么或 {n | R_edit_distance ss n}
中的术语,您实际上没有能力 计算 ,直到您完成一些终止证明。 (例如,证明 forall ss, {n | R_edit_distance ss n}
是 edit_distance
的一种终止证明形式。)
您在某些辅助类型上使用结构递归的想法是完全正确的(这是 唯一的 形式的递归,无论如何都可用;Program
和 Function
只是建立在它的基础上),但它与归纳图没有任何关系......
Fixpoint edit_distance
(s1 s2 : string) (n : nat) (prf : n = String.length s1 + String.length s2)
{struct n}
: nat := _.
按照上面的思路应该可以,但会很混乱。 (您 可以 递归图归纳的实例而不是 nat
这里,但是,同样,这只是开始构建图归纳的实例。)