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

对于 nil 情形的目标,可以通过调用简化器并指示它展开 splitList 的定义来证明,因为空列表的长度小于或等于空列表的长度。 类似地,在 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 的一个证明由 And.intro 构造子应用于如下内容构成:在 left 字段中给出的 A 的证明,以及在 right 字段中给出的 B 的证明。

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 本质上跟踪了 nmNat.le.step 构造子数量上的差异。 因此,该证明应当在基本情形中添加一个额外的 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 构造子会将目标转换为归纳假设:

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,该证明还可以进一步缩短;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 替换为 simp +arith 会移除这些 + 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 的定义或定理都会被标注一个警告。

使用 sorry 给出的 mergeSort 终止性论证的初始草图,可以通过把 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.157191inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xshalves.fst.length < xs.length All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.157191inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:halves.fst.length < xs.length := 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.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := xs.length 2α:Type ?u.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xshalves.fst.length < xs.length α:Type ?u.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsxs.length 2 have : halves.snd.length < xs.length := unsolved goals α:Type ?u.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := this:halves.fst.length < xs.lengthxs.length 2α:Type ?u.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:halves.fst.length < xs.length := splitList_shorter_fst xs sorryhalves.snd.length < xs.length α:Type ?u.189060inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:halves.fst.length < xs.length := splitList_shorter_fst xs 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.189060inst✝: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.220858inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsxs.length 2 All goals completed! 🐙 have : halves.fst.length < xs.length := α:Type ?u.220858inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:xs.length 2 := sorryhalves.fst.length < xs.length α:Type ?u.220858inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:xs.length 2 := sorryxs.length 2 All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.220858inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis✝:xs.length 2 := sorrythis:halves.fst.length < xs.length := splitList_shorter_fst xs this✝halves.snd.length < xs.length α:Type ?u.220858inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis✝:xs.length 2 := sorrythis:halves.fst.length < xs.length := splitList_shorter_fst xs this✝xs.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.254832inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsxs.length 2 All goals completed! 🐙 have : halves.fst.length < xs.length := α:Type ?u.254832inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:xs.length 2 := mergeSort._proof_1 xs hhalves.fst.length < xs.length α:Type ?u.254832inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis:xs.length 2 := mergeSort._proof_1 xs hxs.length 2 All goals completed! 🐙 have : halves.snd.length < xs.length := α:Type ?u.254832inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis✝:xs.length 2 := mergeSort._proof_1 xs hthis:halves.fst.length < xs.length := splitList_shorter_fst xs this✝halves.snd.length < xs.length α:Type ?u.254832inst✝:Ord αxs:List αh:¬xs.length < 2halves:List α × List α := splitList xsthis✝:xs.length 2 := mergeSort._proof_1 xs hthis:halves.fst.length < xs.length := splitList_shorter_fst xs this✝xs.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 的情况下证明以下定理:

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

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

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

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

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

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