Lean 函数式编程

8.4. 更多不等式🔗

Lean 的内置证明自动化足以检查 arrayMapHelperfindHelper 是否停机。 所需要做的就是提供一个值随着每次递归调用而减小的表达式。 但是,Lean 的内置自动化不是万能的,它通常需要一些帮助。

8.4.1. 归并排序🔗

一个停机证明非平凡的函数示例是 List 上的归并排序。归并排序包含两个阶段: 首先,将列表分成两半。使用归并排序对每一半进行排序, 然后使用一个将两个已排序列表合并为一个更大的已排序列表的函数合并结果。 基本情况是空列表和单元素列表,它们都被认为已经排序。

要合并两个已排序列表,需要考虑两个基本情况:

  1. 如果一个输入列表为空,则结果是另一个列表。

  2. 如果两个列表都不为空,则应比较它们的头部。该函数的结果是两个头部中较小的一个, 后面是合并两个列表的剩余项的结果。

这在任何列表上都不是结构化递归。递归停机是因为在每次递归调用中都会从两个列表中的一个中删除一个项, 但它可能是任何一个列表。Lean 在幕后使用这个事实来证明它停机:

def merge [Ord α] (xs : List α) (ys : List α) : List α := match xs, ys with | [], _ => ys | _, [] => xs | x'::xs', y'::ys' => match Ord.compare x' y' with | .lt | .eq => x' :: merge xs' (y' :: ys') | .gt => y' :: merge (x'::xs') ys'

分割列表的一个简单方法是将输入列表中的每个项添加到两个交替的输出列表中:

def splitList (lst : List α) : (List α × List α) := match lst with | [] => ([], []) | x :: xs => let (a, b) := splitList xs (x :: b, a)

这个分割函数是结构化递归的。

归并排序检查是否已达到基本情况。如果是,则返回输入列表。 如果不是,则分割输入,并合并对每一半排序的结果:

def fail to show termination for mergeSort with errors failed to infer structural recursion: Not considering parameter α of mergeSort: it is unchanged in the recursive calls Not considering parameter #2 of mergeSort: it is unchanged in the recursive calls Cannot use parameter xs: failed to eliminate recursive application mergeSort halves.fst Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) xs #1 1) 70:11-31 ? ? 2) 70:34-54 _ _ #1: xs.length Please use `termination_by` to specify a decreasing measure.mergeSort [Ord α] (xs : List α) : List α := if unused variable `h` Note: This linter can be disabled with `set_option linter.unusedVariables false`h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs merge (mergeSort halves.fst) (mergeSort halves.snd)

Lean 的模式匹配编译器能够判断由测试 xs.length < 2if 引入的前提 h 排除了长度超过一个条目的列表,因此没有“缺少情况”的错误。 然而,即使此程序总是停机,它也不是结构化递归的,并且 Lean 无法自动发现递减度量:

fail to show termination for
  mergeSort
with errors
failed to infer structural recursion:
Not considering parameter α of mergeSort:
  it is unchanged in the recursive calls
Not considering parameter #2 of mergeSort:
  it is unchanged in the recursive calls
Cannot use parameter xs:
  failed to eliminate recursive application
    mergeSort halves.fst


Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
            xs #1
1) 70:11-31  ?  ?
2) 70:34-54  _  _

#1: xs.length

Please use `termination_by` to specify a decreasing measure.

它能停机的原因是 splitList 总是返回比其输入更短的列表,至少在应用于包含至少两个元素的列表时是这样。 因此,halves.fsthalves.snd 的长度小于 xs 的长度。 这可以使用 termination_by 子句来表示:

def mergeSort [Ord α] (xs : List α) : List α := if unused variable `h` Note: This linter can be disabled with `set_option linter.unusedVariables false`h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs merge (failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal α:Type u_1xs:List αh:¬xs.length < 2halves:List α × List α := splitList xs(splitList xs).fst.length < xs.lengthmergeSort halves.fst) (mergeSort halves.snd) termination_by xs.length

有了这个子句,错误信息就变了。 Lean 不会抱怨函数不是结构化递归的,而是指出它无法自动证明 (splitList xs).fst.length < xs.length

failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
α:Type u_1xs:List αh:¬xs.length < 2halves:List α × List α := splitList xs(splitList xs).fst.length < xs.length

8.4.2. 分割列表使其变短🔗

还需要证明 (splitList xs).snd.length < xs.length。 由于 splitList 在向两个列表添加条目之间交替进行,因此最简单的方法是同时证明这两个语句,这样证明的结构就可以遵循用于实现 splitList 的算法。 换句话说,最简单的方法是证明 (lst : List α), (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length

不幸的是,这个陈述是错误的。 特别是,splitList []([], [])。两个输出列表的长度都是 0,这并不小于输入列表的长度 0。 类似地,splitList ["basalt"] 求值为 (["basalt"], []),而 ["basalt"] 并不比 ["basalt"] 短。 然而,splitList ["basalt", "granite"] 求值为 (["basalt"], ["granite"]),这两个输出列表都比输入列表短。

事实证明,输出列表的长度始终小于或等于输入列表的长度,但仅当输入列表至少包含两个条目时,它们才严格更短。 事实证明,最容易证明前一个陈述,然后将其扩展到后一个陈述。 从定理的陈述开始:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := unsolved goals α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.lengthα:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length
unsolved goals
α:Type u_1lst:List α(splitList lst).fst.length  lst.length  (splitList lst).snd.length  lst.length

由于 splitList 在列表上是结构化递归的,因此证明应使用归纳法。 splitList 中的结构化递归非常适合归纳证明:归纳法的基本情况与递归的基本情况匹配, 归纳步骤与递归调用匹配。induction 策略给出了两个目标:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length
unsolved goals
α:Type u_1(splitList []).fst.length  [].length  (splitList []).snd.length  [].length
unsolved goals
α:Type u_1x:αxs:List αih:(splitList xs).fst.length  xs.length  (splitList xs).snd.length  xs.length(splitList (x :: xs)).fst.length  (x :: xs).length  (splitList (x :: xs)).snd.length  (x :: xs).length

可以通过调用简化器并指示它展开 splitList 的定义来证明 nil 情况的目标,因为空列表的长度小于或等于空列表的长度。 类似地,在 cons 情况下使用 splitList 简化会在目标中的长度周围放置 Nat.succ

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length All goals completed! 🐙 α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length
unsolved goals
α:Type u_1x:αxs:List αih:(splitList xs).fst.length  xs.length  (splitList xs).snd.length  xs.length(splitList xs).snd.length  xs.length  (splitList xs).fst.length  xs.length + 1

这是因为对 List.length 的调用消耗了列表 x :: xs 的头部,将其转换为 Nat.succ,既在输入列表的长度中,也在第一个输出列表的长度中。

在 Lean 中编写 A BAnd A B 的缩写。 AndProp 宇宙中的一个结构体类型:

structure And (a b : Prop) : Prop where intro :: left : a right : b

换句话说,A B 的证明包括应用于 left 字段中 A 的证明和应用于 right 字段中 B 的证明的 And.intro 构造子。

cases 策略允许证明依次考虑数据类型的每个构造子或命题的每个潜在证明。 它对应于没有递归的 match 表达式。 对结构体使用 cases 会导致结构体被分解,并为结构体的每个字段添加一个假设,就像模式匹配表达式提取结构体的字段以用于程序中一样。 由于结构体只有一个构造子,因此对结构体使用 cases 不会产生额外的目标。

由于 ihList.length (splitList xs).fst ≤ List.length xs ∧ List.length (splitList xs).snd ≤ List.length xs 的一个证明, 使用 cases ih 会产生一个 List.length (splitList xs).fst ≤ List.length xs 的假设和一个 List.length (splitList xs).snd ≤ List.length xs 的假设:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length All goals completed! 🐙 α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length
unsolved goals
α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length  xs.lengthright✝:(splitList xs).snd.length  xs.length(splitList xs).snd.length  xs.length  (splitList xs).fst.length  xs.length + 1

由于证明的目标也是一个 And,因此可以使用 constructor 策略应用 And.intro, 从而为每个参数生成一个目标:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length All goals completed! 🐙 α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length
unsolved goals
α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length  xs.lengthright✝:(splitList xs).snd.length  xs.length(splitList xs).snd.length  xs.length

α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length  xs.lengthright✝:(splitList xs).snd.length  xs.length(splitList xs).fst.length  xs.length + 1

left 目标与 left✝ 假设非常相似,所以 assumption 策略将其分派:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length All goals completed! 🐙 α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length
unsolved goals
α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length  xs.lengthright✝:(splitList xs).snd.length  xs.length(splitList xs).fst.length  xs.length + 1

同样,right 目标类似于 right✝ 假设,除了目标仅将 + 1 添加到输入列表的长度。 现在是时候证明不等式成立了。

8.4.2.1. 在较大的一侧加一🔗

证明 splitList_shorter_le 所需的不等式是 (n m : Nat), n m n m + 1。 传入的假设 n m 本质上是在 Nat.le.step 构造子的数量上跟踪 nm 之间的差异。 因此,证明应该在基本情况中添加一个额外的 Nat.le.step

开始时,该陈述如下:

theorem Nat.le_succ_of_le : n m n m + 1 := unsolved goals n m:Natn m n m + 1n:Natm:Natn m n m + 1 n:Natm:Natn m n m + 1
unsolved goals
n m:Natn  m  n  m + 1

第一步是为假设 n m 引入一个名称:

theorem Nat.le_succ_of_le : n m n m + 1 := unsolved goals n m:Nath:n mn m + 1n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1
unsolved goals
n m:Nath:n  mn  m + 1

证明是通过对该假设进行归纳来进行的:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n m✝.succ + 1

refl 的情况下,其中 n = m,目标是证明 n ≤ n + 1

unsolved goals
n m:Natn  n + 1

step 的情况下,目标是在假设 n m 下证明 n m + 1

unsolved goals
n m m✝:Nata✝:n.le m✝ih:n  m✝ + 1n  m✝.succ + 1

对于 refl 情况,可以应用 step 构造子:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n m✝.succ + 1
unsolved goals
n m:Natn.le n

step 之后,可以使用 refl,这只留下了 step 的目标:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natn.le n; All goals completed! 🐙 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n m✝.succ + 1
unsolved goals
n m m✝:Nata✝:n.le m✝ih:n  m✝ + 1n  m✝.succ + 1

对于 step,应用 step 构造子将目标转换为归纳假设:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natn.le n; All goals completed! 🐙 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n m✝.succ + 1
unsolved goals
n m m✝:Nata✝:n.le m✝ih:n  m✝ + 1n.le (m✝ + 1)

最终证明如下:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natn.le n; All goals completed! 🐙 n:Natm:Natm✝:Nata✝:n.le m✝a_ih✝:n m✝ + 1n m✝.succ + 1 n:Natm:Natm✝:Nata✝:n.le m✝a_ih✝:n m✝ + 1n.le (m✝ + 1); All goals completed! 🐙

为了揭示幕后发生的事情,applyexact 策略可用于准确指示正在应用哪个构造子。 apply 策略通过应用一个返回类型匹配的函数或构造子来解决当前目标,为每个未提供的参数创建新的目标,而如果需要任何新目标,exact 就会失败:

theorem Nat.le_succ_of_le : n m n m + 1 := n:Natm:Natn m n m + 1 n:Natm:Nath:n mn m + 1 induction h with n:Natm:Natn n + 1 n:Natm:Natn.le n; All goals completed! 🐙 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n m✝.succ + 1 n:Natm:Natm✝:Nata✝:n.le m✝ih:n m✝ + 1n.le (m✝ + 1); All goals completed! 🐙

证明可以简化:

theorem Nat.le_succ_of_le (h : n m) : n m + 1:= n:Natm:Nath:n mn m + 1 n:Natm:Natn n + 1n:Natm:Natm✝:Nata✝:n.le m✝a_ih✝:n m✝ + 1n m✝.succ + 1 n:Natm:Natn n + 1n:Natm:Natm✝:Nata✝:n.le m✝a_ih✝:n m✝ + 1n m✝.succ + 1 repeat (first | n:Natm:Natm✝:Nata✝:n.le m✝a_ih✝:n m✝ + 1n.le m✝ | All goals completed! 🐙)

在这个简短的策略脚本中,由 induction 引入的两个目标都使用 repeat (first | constructor | assumption) 来解决。 策略 first | T1 | T2 | ... | Tn 表示按顺序尝试 T1Tn,然后使用第一个成功的策略。 换句话说,repeat (first | constructor | assumption) 会尽可能地应用构造子,然后尝试使用假设来解决目标。

通过使用 grind,证明可以进一步缩短,它包含一个线性算术求解器:

theorem Nat.le_succ_of_le (h : n m) : n m + 1:= n:Natm:Nath:n mn m + 1 All goals completed! 🐙

最后,证明可以写成一个递归函数:

theorem Nat.le_succ_of_le : n m n m + 1 | .refl => .step .refl | .step h => .step (Nat.le_succ_of_le h)

每种证明风格都适用于不同的情况。 详细的证明脚本在初学者阅读代码或证明步骤提供某种见解的情况下很有用。 简短、高度自动化的证明脚本通常更容易维护,因为自动化通常在面对定义和数据类型的细微更改时既灵活又健壮。 递归函数通常从数学证明的角度来看更难理解,也更难维护,但对于开始使用交互式定理证明的程序员来说,它可能是一个有用的桥梁。

8.4.2.2. 完成证明🔗

现在已经证明了两个辅助定理,splitList_shorter_le 的其余部分将很快完成。 当前的证明状态还剩下一个目标:

unsolved goals
α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length  xs.lengthright✝:(splitList xs).snd.length  xs.length(splitList xs).fst.length  xs.length + 1

使用 Nat.le_succ_of_le 以及 right✝ 假设完成了证明:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length induction lst with α:Type u_1(splitList []).fst.length [].length (splitList []).snd.length [].length All goals completed! 🐙 α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList (x :: xs)).fst.length (x :: xs).length (splitList (x :: xs)).snd.length (x :: xs).length α:Type u_1x:αxs:List αih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(splitList xs).snd.length xs.length (splitList xs).fst.length xs.length + 1 α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).snd.length xs.length (splitList xs).fst.length xs.length + 1 α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).snd.length xs.lengthα:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).fst.length xs.length + 1 case left α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).snd.length xs.length All goals completed! 🐙 case right α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).fst.length xs.length + 1 α:Type u_1x:αxs:List αleft✝:(splitList xs).fst.length xs.lengthright✝:(splitList xs).snd.length xs.length(splitList xs).fst.length xs.length All goals completed! 🐙

下一步是返回到证明归并排序停机所需的实际定理:只要一个列表至少有两个条目,则分割它的两个结果都严格短于它。

theorem splitList_shorter (lst : List α) (_ : lst.length 2) : (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length := unsolved goals α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.lengthα:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length
unsolved goals
α:Type u_1lst:List αx✝:lst.length  2(splitList lst).fst.length < lst.length  (splitList lst).snd.length < lst.length

模式匹配在策略脚本中与在程序中一样有效。 因为 lst 至少有两个条目,所以它们可以用 match 暴露出来,它还通过依值模式匹配来细化类型:

theorem splitList_shorter (lst : List α) (_ : lst.length 2) : (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length := α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length match lst with α:Type u_1lst:List αx:αy:αxs:List αx✝:(x :: y :: xs).length 2(splitList (x :: y :: xs)).fst.length < (x :: y :: xs).length (splitList (x :: y :: xs)).snd.length < (x :: y :: xs).length
unsolved goals
α:Type u_1lst:List αx y:αxs:List αx✝:(x :: y :: xs).length  2(splitList (x :: y :: xs)).fst.length < (x :: y :: xs).length 
  (splitList (x :: y :: xs)).snd.length < (x :: y :: xs).length

使用 splitList 简化会删除 xy,导致列表的计算长度每个都获得 + 1

theorem splitList_shorter (lst : List α) (_ : lst.length 2) : (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length := α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length match lst with α:Type u_1lst:List αx:αy:αxs:List αx✝:(x :: y :: xs).length 2(splitList (x :: y :: xs)).fst.length < (x :: y :: xs).length (splitList (x :: y :: xs)).snd.length < (x :: y :: xs).length
unsolved goals
α:Type u_1lst:List αx y:αxs:List αx✝:(x :: y :: xs).length  2(splitList xs).fst.length < xs.length + 1  (splitList xs).snd.length < xs.length + 1

simp +arith 替换 simp 会删除这些 + 1,因为 simp +arith 利用了 n + 1 < m + 1 意味着 n < m 的事实:

theorem splitList_shorter (lst : List α) (_ : lst.length 2) : (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length := α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length match lst with α:Type u_1lst:List αx:αy:αxs:List αx✝:(x :: y :: xs).length 2(splitList (x :: y :: xs)).fst.length < (x :: y :: xs).length (splitList (x :: y :: xs)).snd.length < (x :: y :: xs).length
unsolved goals
α:Type u_1lst:List αx y:αxs:List αx✝:(x :: y :: xs).length  2(splitList xs).fst.length  xs.length  (splitList xs).snd.length  xs.length

此目标现在匹配 splitList_shorter_le,可用于结束证明:

theorem splitList_shorter (lst : List α) (_ : lst.length 2) : (splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length := α:Type u_1lst:List αx✝:lst.length 2(splitList lst).fst.length < lst.length (splitList lst).snd.length < lst.length match lst with α:Type u_1lst:List αx:αy:αxs:List αx✝:(x :: y :: xs).length 2(splitList (x :: y :: xs)).fst.length < (x :: y :: xs).length (splitList (x :: y :: xs)).snd.length < (x :: y :: xs).length α:Type u_1lst:List αx:αy:αxs:List αx✝:(x :: y :: xs).length 2(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length All goals completed! 🐙

证明 mergeSort 停机所需的事实可以从结果 And 中提取出来:

theorem splitList_shorter_fst (lst : List α) (h : lst.length 2) : (splitList lst).fst.length < lst.length := splitList_shorter lst h |>.left theorem splitList_shorter_snd (lst : List α) (h : lst.length 2) : (splitList lst).snd.length < lst.length := splitList_shorter lst h |>.right

8.4.2.3. 一个更简单的证明🔗

除了使用普通归纳法,splitList_shorter_le 还可以使用函数归纳法来证明,从而为 splitList 的每个分支产生一个情况:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length fun_induction splitList with α:Type u_1([], []).fst.length [].length ([], []).snd.length [].length α:Type u_1x:αxs:List αa:List αb:List αsplitEq:splitList xs = (a, b)ih:(splitList xs).fst.length xs.length (splitList xs).snd.length xs.length(x :: b, a).fst.length (x :: xs).length (x :: b, a).snd.length (x :: xs).length

第一种情况匹配 splitList 的基本情况。 splitList两个 应用都被替换为第一个分支的结果:

unsolved goals
α:Type u_1([], []).fst.length  [].length  ([], []).snd.length  [].length

第二种情况匹配 splitList 的递归分支。 除了归纳假设之外,splitListlet 的值也在假设中被跟踪:

unsolved goals
α:Type u_1x:αxs a b:List αsplitEq:splitList xs = (a, b)ih:(splitList xs).fst.length  xs.length  (splitList xs).snd.length  xs.length(x :: b, a).fst.length  (x :: xs).length  (x :: b, a).snd.length  (x :: xs).length

虽然第二种情况看起来有点复杂,但完成证明所需的一切都已存在。 实际上,grind 可以立即证明这两个目标:

theorem splitList_shorter_le (lst : List α) : (splitList lst).fst.length lst.length (splitList lst).snd.length lst.length := α:Type u_1lst:List α(splitList lst).fst.length lst.length (splitList lst).snd.length lst.length α:Type u_1([], []).fst.length [].length ([], []).snd.length [].lengthα:Type u_1x✝¹:αxs✝:List αa✝:List αb✝:List αx✝:splitList xs✝ = (a✝, b✝)ih1✝:(splitList xs✝).fst.length xs✝.length (splitList xs✝).snd.length xs✝.length(x✝¹ :: b✝, a✝).fst.length (x✝¹ :: xs✝).length (x✝¹ :: b✝, a✝).snd.length (x✝¹ :: xs✝).length α:Type u_1([], []).fst.length [].length ([], []).snd.length [].lengthα:Type u_1x✝¹:αxs✝:List αa✝:List αb✝:List αx✝:splitList xs✝ = (a✝, b✝)ih1✝:(splitList xs✝).fst.length xs✝.length (splitList xs✝).snd.length xs✝.length(x✝¹ :: b✝, a✝).fst.length (x✝¹ :: xs✝).length (x✝¹ :: b✝, a✝).snd.length (x✝¹ :: xs✝).length All goals completed! 🐙

8.4.3. 归并排序停机证明🔗

归并排序有两个递归调用,一个用于 splitList 返回的每个子列表。 每个递归调用都需要证明传递给它的列表的长度短于输入列表的长度。 通常分两步编写停机证明会更方便:首先,写下允许 Lean 验证停机的命题,然后证明它们。 否则,可能会投入大量精力来证明命题,却发现它们并不是所需的在更小的输入上建立递归调用的内容。

sorry 策略可以证明任何目标,即使是错误的目标。 它不适用于生产代码或最终证明,但它是一种便捷的方法,可以提前“勾勒出”证明或程序。 任何使用 sorry 的定义或定理都会附有警告。

使用 sorrymergeSort 停机论证的初始草图可以通过将 Lean 无法证明的目标复制到 have 表达式中来编写。 在 Lean 中,have 类似于 let。 当使用 have 时,名称是可选的。 通常,let 用于定义引用关键值的名称,而 have 用于局部证明命题,当 Lean 在寻找“数组查找是否在范围内”或“函数是否停机”的证据时,可以找到这些命题。

declaration uses 'sorry'def declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'mergeSort [Ord α] (xs : List α) : List α := if h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs have : halves.fst.length < xs.length := α:Type ?u.107926inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.107934 × List _fvar.107934 := splitList _fvar.107936halves.fst.length < xs.length All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.107926inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.107934 × List _fvar.107934 := splitList _fvar.107936this:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := sorryhalves.snd.length < xs.length All goals completed! 🐙 merge (mergeSort halves.fst) (mergeSort halves.snd) termination_by xs.length

警告位于名称 mergeSort 上:

declaration uses 'sorry'

因为没有错误,所以建议的命题足以建立停机证明。

证明从应用辅助定理开始:

def mergeSort [Ord α] (xs : List α) : List α := if h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs have : halves.fst.length < xs.length := unsolved goals α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := xs.length 2α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.128119 × List _fvar.128119 := splitList _fvar.128121halves.fst.length < xs.length α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.128119 × List _fvar.128119 := splitList _fvar.128121xs.length 2 have : halves.snd.length < xs.length := unsolved goals α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := this:halves.fst.length < xs.lengthxs.length 2α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.128119 × List _fvar.128119 := splitList _fvar.128121this:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.128121 sorryhalves.snd.length < xs.length α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.128119 × List _fvar.128119 := splitList _fvar.128121this:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.128121 sorryxs.length 2 merge (mergeSort halves.fst) (mergeSort halves.snd) termination_by xs.length

两个证明都失败了,因为 splitList_shorter_fstsplitList_shorter_snd 都需要证明 xs.length 2

unsolved goals
α:Type ?u.128111inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := xs.length  2

要检查这是否足以完成证明,请使用 sorry 添加它并检查错误:

declaration uses 'sorry'def declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'mergeSort [Ord α] (xs : List α) : List α := if h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs have : xs.length 2 := α:Type ?u.148312inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.148320 × List _fvar.148320 := splitList _fvar.148322xs.length 2 All goals completed! 🐙 have : halves.fst.length < xs.length := α:Type ?u.148312inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.148320 × List _fvar.148320 := splitList _fvar.148322this:List.length _fvar.148322 2 := sorryhalves.fst.length < xs.length α:Type ?u.148312inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.148320 × List _fvar.148320 := splitList _fvar.148322this:List.length _fvar.148322 2 := sorryxs.length 2 All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.148312inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.148320 × List _fvar.148320 := splitList _fvar.148322this✝:List.length _fvar.148322 2 := sorrythis:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.148322 _fvar.149158halves.snd.length < xs.length α:Type ?u.148312inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.148320 × List _fvar.148320 := splitList _fvar.148322this✝:List.length _fvar.148322 2 := sorrythis:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.148322 _fvar.149158xs.length 2 All goals completed! 🐙 merge (mergeSort halves.fst) (mergeSort halves.snd) termination_by xs.length

同样,只会有一个警告。

declaration uses 'sorry'

有一个有希望的假设可用:h : ¬List.length xs < 2,它来自 if。 显然,如果不是 xs.length < 2,那么 xs.length 2grind 策略解决了这个目标,程序现在已完成:

def mergeSort [Ord α] (xs : List α) : List α := if h : xs.length < 2 then match xs with | [] => [] | [x] => [x] else let halves := splitList xs have : xs.length 2 := α:Type ?u.169458inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.169466 × List _fvar.169466 := splitList _fvar.169468xs.length 2 All goals completed! 🐙 have : halves.fst.length < xs.length := α:Type ?u.169458inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.169466 × List _fvar.169466 := splitList _fvar.169468this:List.length _fvar.169468 2 := mergeSort._proof_1 _fvar.169468 _fvar.170262halves.fst.length < xs.length α:Type ?u.169458inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.169466 × List _fvar.169466 := splitList _fvar.169468this:List.length _fvar.169468 2 := mergeSort._proof_1 _fvar.169468 _fvar.170262xs.length 2 All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.169458inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.169466 × List _fvar.169466 := splitList _fvar.169468this✝:List.length _fvar.169468 2 := mergeSort._proof_1 _fvar.169468 _fvar.170262this:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.169468 _fvar.170304halves.snd.length < xs.length α:Type ?u.169458inst✝:Ord αxs:List αh:¬xs.length < 2halves:List _fvar.169466 × List _fvar.169466 := splitList _fvar.169468this✝:List.length _fvar.169468 2 := mergeSort._proof_1 _fvar.169468 _fvar.170262this:failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) := splitList_shorter_fst _fvar.169468 _fvar.170304xs.length 2 All goals completed! 🐙 merge (mergeSort halves.fst) (mergeSort halves.snd) termination_by xs.length

该函数可以在示例上进行测试:

["geode", "limestone", "mica", "soapstone"]#eval mergeSort ["soapstone", "geode", "mica", "limestone"]
["geode", "limestone", "mica", "soapstone"]
[3, 5, 15, 22]#eval mergeSort [5, 3, 22, 15]
[3, 5, 15, 22]

8.4.4. 用减法迭代表示除法🔗

正如乘法是迭代的加法,指数是迭代的乘法,除法可以理解为迭代的减法。 本书中对递归函数的第一个描述给出了除法的一个版本,当除数不为零时停机,但 Lean 并不接受。 证明除法终止需要使用关于不等式的事实。

Lean 无法证明除法的这个定义会停机:

def fail to show termination for div with errors failed to infer structural recursion: Not considering parameter k of div: it is unchanged in the recursive calls Cannot use parameter k: failed to eliminate recursive application div (n - k) k failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal k n:Nath✝:¬n < kn - k < ndiv (n k : Nat) : Nat := if n < k then 0 else 1 + div (n - k) k
fail to show termination for
  div
with errors
failed to infer structural recursion:
Not considering parameter k of div:
  it is unchanged in the recursive calls
Cannot use parameter k:
  failed to eliminate recursive application
    div (n - k) k


failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
k n:Nath✝:¬n < kn - k < n

这是一件好事,因为它确实不会停机! 当 k0 时,n 的值不会减小,因此程序是一个无限循环。

重写该函数以获取 k 不为 0 的证据,允许 Lean 自动证明停机:

def div (n k : Nat) (ok : k 0) : Nat := if h : n < k then 0 else 1 + div (n - k) k ok

div 的这个定义会停机,因为第一个参数 n 在每次递归调用时都更小。 这可以使用 termination_by 子句来表示:

def div (n k : Nat) (ok : k 0) : Nat := if h : n < k then 0 else 1 + div (n - k) k ok termination_by n

8.4.5. 练习🔗

在不使用 grind 的情况下证明以下定理:

  • 对于所有自然数 n0 < n + 1

  • 对于所有自然数 n0 \leq n

  • 对于所有自然数 nk(n + 1) - (k + 1) = n - k

  • 对于所有自然数 nk,如果 k < nn \neq 0

  • 对于所有自然数 nn - n = 0

  • 对于所有自然数 nk,如果 n + 1 < kn < k