Lean 4 定理证明

8. 归纳和递归🔗

在上一章中,我们看到归纳定义提供了在 Lean 中引入新类型的强大手段。此外, 构造子和递归器提供了在这些类型上定义函数的唯一手段。根据 命题即类型 对应关系, 这意味着归纳法是证明的基本方法。

Lean 提供了定义递归函数、执行模式匹配和编写归纳证明的自然方法。它允许你通过指定它应该满足的方程来定义一个函数, 它允许你通过指定如何处理可能出现的各种情况来证明一个定理。在它内部,这些描述被“编译”成原始递归器, 使用一个我们称之为“方程编译器”的程序。方程编译器不是可信代码库的一部分;它的输出包括由内核独立检查的项。

8.1. 模式匹配🔗

对示意图模式的解释是编译过程的第一步。我们已经看到,casesOn 递归器可以通过分情况讨论来定义函数和证明定理, 根据归纳定义类型所涉及的构造子。但是复杂的定义可能会使用几个嵌套的 casesOn 应用, 而且可能很难阅读和理解。模式匹配提供了一种更方便的方法,并且为函数式编程语言的用户所熟悉。

考虑一下自然数的归纳定义类型。每个自然数要么是 zero,要么是 succ x, 因此你可以通过在每个情况下指定一个值来定义一个从自然数到任意类型的函数:

open Nat def sub1 : Nat Nat | zero => zero | succ x => x def isZero : Nat Bool | zero => true | succ x => false

用来定义这些函数的方程在定义上是成立的:

example : sub1 0 = 0 := rfl example (x : Nat) : sub1 (succ x) = x := rfl example : isZero 0 = true := rfl example (x : Nat) : isZero (succ x) = false := rfl example : sub1 7 = 6 := rfl example (x : Nat) : isZero (x + 3) = false := rfl

我们可以用一些更耳熟能详的符号,而不是 zerosucc

def sub1 : Nat Nat | 0 => 0 | x + 1 => x def isZero : Nat Bool | 0 => true | x + 1 => false

因为加法和零符号已经被赋予 [match_pattern] 属性,它们可以被用于模式匹配。Lean 简单地将这些表达式规范化, 直到显示构造子 zerosucc

模式匹配适用于任何归纳类型,如乘积和 Option 类型:

def swap : α × β β × α | (a, b) => (b, a) def foo : Nat × Nat Nat | (m, n) => m + n def bar : Option Nat Nat | some n => n + 1 | none => 0

在这里,我们不仅用它来定义一个函数,而且还用它来进行逐情况证明:

def not : Bool Bool | true => false | false => true theorem not_not : (b : Bool), not (not b) = b | true => show not (not true) = true from rfl | false => show not (not false) = false from rfl

模式匹配也可以用来解构归纳定义的命题:

example (p q : Prop) : p q q p | And.intro h₁ h₂ => And.intro h₂ h₁ example (p q : Prop) : p q q p | Or.inl hp => Or.inr hp | Or.inr hq => Or.inl hq

这样解决带逻辑连接词的命题就很紧凑。

在所有这些例子中,模式匹配被用来进行单一情况的区分。更有趣的是,模式可以涉及嵌套的构造子,如下面的例子。

def sub2 : Nat Nat | 0 => 0 | 1 => 0 | x + 2 => x

方程编译器首先对输入是 zero 还是 succ x 的形式进行分类讨论,然后对 xzero 还是 succ x 的形式进行分类讨论。 它从提交给它的模式中确定必要的情况拆分,如果模式不能穷尽情况,则会引发错误。同时,我们可以使用算术符号,如下面的版本。 在任何一种情况下,定义方程都是成立的。

example : sub2 0 = 0 := rfl example : sub2 1 = 0 := rfl example : sub2 (x+2) = x := rfl example : sub2 5 = 3 := rfl

你可以写 def sub2 : Nat Nat := fun x => match x with | 0 => 0 | 1 => 0 | x.succ.succ => x#print sub2 来看看这个函数是如何被编译成递归器的。(Lean 会告诉你 sub2 已经被定义为内部辅助函数 sub2.match_1, 但你也可以把它打印出来)。Lean 使用这些辅助函数来编译 match 表达式。实际上,上面的定义被扩展为

def sub2 : Nat Nat := fun x => match x with | 0 => 0 | 1 => 0 | x + 2 => x

下面是一些嵌套模式匹配的例子:

example (p q : α Prop) : ( x, p x q x) ( x, p x) ( x, q x) | Exists.intro x (Or.inl px) => Or.inl (Exists.intro x px) | Exists.intro x (Or.inr qx) => Or.inr (Exists.intro x qx) def foo : Nat × Nat Nat | (0, n) => 0 | (m+1, 0) => 1 | (m+1, n+1) => 2

方程编译器可以按顺序处理多个参数。例如,将前面的例子定义为两个参数的函数会更自然:

def foo : Nat Nat Nat | 0, n => 0 | m + 1, 0 => 1 | m + 1, n + 1 => 2

另一例:

def bar : List Nat List Nat Nat | [], [] => 0 | a :: as, [] => a | [], b :: bs => b | a :: as, b :: bs => a + b

这些模式是由逗号分隔的。

在下面的每个例子中,尽管其他参数包括在模式列表中,但只对第一个参数进行了分割。

def and : Bool Bool Bool | true, a => a | false, _ => false def or : Bool Bool Bool | true, _ => true | false, a => a def cond : Bool α α α | true, x, y => x | false, x, y => y

还要注意的是,当定义中不需要一个参数的值时,你可以用下划线来代替。这个下划线被称为 通配符模式 ,或 匿名变量 。与方程编译器之外的用法不同,这里的下划线 并不 表示一个隐参数。使用下划线表示通配符在函数式编程语言中是很常见的,所以 Lean 采用了这种符号。通配符和重叠模式一节阐述了通配符的概念,而不可访问模式一节解释了你如何在模式中使用隐参数。

正如归纳类型一章中所描述的,归纳数据类型可以依赖于参数。下面的例子使用模式匹配定义了 tail 函数。参数 α : Type u 是一个参数,出现在冒号之前,表示它不参与模式匹配。Lean 也允许参数出现在 : 之后,但需要显式的 match 才能模式匹配。

def tail1 {α : Type u} : List α List α | [] => [] | a :: as => as def tail2 : {α : Type u} List α List α | α, [] => [] | α, a :: as => as

尽管参数 α 在这两个例子中的位置不同,但在这两种情况下,它的处理方式是一样的,即它不参与情况分割。

Lean 也可以处理更复杂的模式匹配形式,其中从属类型的参数对各种情况构成了额外的约束。 这种 依值模式匹配 的例子在 依值模式匹配一节中考虑。

8.2. 通配符和重叠模式🔗

考虑上节的一个例子:

def foo : Nat Nat Nat | 0, n => 0 | m + 1, 0 => 1 | m + 1, n + 1 => 2

也可以表述成:

def foo : Nat Nat Nat | 0, n => 0 | m, 0 => 1 | m, n => 2

在第二种表述中,模式是重叠的;例如,一对参数 0, 0 符合所有三种情况。但是 Lean 通过使用第一个适用的方程来处理这种模糊性, 所以在这个例子中,最终结果是一样的。特别是,以下方程在定义上是成立的:

example : foo 0 0 = 0 := rfl example : foo 0 (n + 1) = 0 := rfl example : foo (m + 1) 0 = 1 := rfl example : foo (m + 1) (n + 1) = 2 := rfl

由于不需要 mn 的值,我们也可以用通配符模式代替。

def foo : Nat Nat Nat | 0, _ => 0 | _, 0 => 1 | _, _ => 2

你可以检查这个 foo 的定义是否满足与之前相同的定义特性。

一些函数式编程语言支持 不完整的模式 。在这些语言中,解释器对不完整的情况产生一个异常或返回一个任意的值。 我们可以使用 Inhabited (含元素的)类型类来模拟任意值的方法。粗略的说,Inhabited α 的一个元素是对 α 拥有一个元素的见证; 在 类型类一章中,我们将看到 Lean 可以被告知合适的基础类型是含元素的,并且可以自动推断出其他构造类型是含元素的。 在此基础上,标准库提供了一个任意元素 default,任何含元素的类型。

我们还可以使用类型 Option α 来模拟不完整的模式。我们的想法是对所提供的模式返回 some a, 而对不完整的情况使用 none。下面的例子演示了这两种方法。

def f1 : Nat Nat Nat | 0, _ => 1 | _, 0 => 2 | _, _ => default -- the "incomplete" case example : f1 0 0 = 1 := rfl example : f1 0 (a+1) = 1 := rfl example : f1 (a+1) 0 = 2 := rfl example : f1 (a+1) (b+1) = default := rfl def f2 : Nat Nat Option Nat | 0, _ => some 1 | _, 0 => some 2 | _, _ => none -- the "incomplete" case example : f2 0 0 = some 1 := rfl example : f2 0 (a+1) = some 1 := rfl example : f2 (a+1) 0 = some 2 := rfl example : f2 (a+1) (b+1) = none := rfl

方程编译器是很智能的。如果你遗漏了以下定义中的任何一种情况,错误信息会告诉你遗漏了哪个。

def bar : Nat List Nat Bool Nat | 0, _, false => 0 | 0, b :: _, _ => b | 0, [], true => 7 | a+1, [], false => a | a+1, [], true => a + 1 | a+1, b :: _, _ => a + b

某些情况也可以用 if ... then ... else 代替 casesOn

def foo : Char Nat | 'A' => 1 | 'B' => 2 | _ => 3 def foo.match_1.{u_1} : (motive : Char Sort u_1) (x : Char) (Unit motive 'A') (Unit motive 'B') ((x : Char) motive x) motive x := fun motive x h_1 h_2 h_3 => if h_1_1 : x = 'A' then Eq.symm h_1_1 Eq.symm h_1_1 h_1 () else if h_2_1 : x = 'B' then Eq.ndrec (motive := fun x => ¬x = 'A' motive x) (fun h_1 => Eq.symm h_2_1 h_2 ()) (Eq.symm h_2_1) h_1_1 else h_3 x#print foo.match_1
def foo.match_1.{u_1} : (motive : Char  Sort u_1) 
  (x : Char)  (Unit  motive 'A')  (Unit  motive 'B')  ((x : Char)  motive x)  motive x :=
fun motive x h_1 h_2 h_3 =>
  if h_1_1 : x = 'A' then Eq.symm h_1_1  Eq.symm h_1_1  h_1 ()
  else
    if h_2_1 : x = 'B' then
      Eq.ndrec (motive := fun x => ¬x = 'A'  motive x) (fun h_1 => Eq.symm h_2_1  h_2 ()) (Eq.symm h_2_1) h_1_1
    else h_3 x

8.3. 结构化递归和归纳🔗

方程编译器的强大之处在于,它还支持递归定义。在接下来的三节中,我们将分别介绍。

  • 结构性递归定义

  • 良基的递归定义

  • 相互递归的定义

一般来说,方程编译器处理以下形式的输入:

def foo (a : α) : (b : β) → γ
  | [patterns₁] => t₁
  ...
  | [patternsₙ] => tₙ

这里 (a : α) 是一个参数序列,(b : β) 是进行模式匹配的参数序列,γ 是任何类型, 它可以取决于 ab。每一行应该包含相同数量的模式,对应于 β 的每个元素。 正如我们所看到的,模式要么是一个变量,要么是应用于其他模式的构造子,要么是一个正规化为该形式的表达式 (其中非构造子用 [match_pattern] 属性标记)。构造子的出现会提示情况拆分,构造子的参数由给定的变量表示。 在 依值模式匹配一节中,我们将看到有时有必要在模式中包含明确的项, 这些项需要进行表达式类型检查,尽管它们在模式匹配中没有起到作用。由于这个原因,这些被称为“不可访问的模式”。 但是在 依值模式匹配一节之前,我们将不需要使用这种不可访问的模式。

正如我们在上一节所看到的,项 t₁, ..., tₙ 可以利用任何一个参数 a,以及在相应模式中引入的任何一个变量。 使得递归和归纳成为可能的是,它们也可以涉及对 foo 的递归调用。在本节中,我们将处理 结构性递归 , 其中 foo 的参数出现在 => 的右侧,是左侧模式的子项。我们的想法是,它们在结构上更小, 因此在归纳类型中出现在更早的阶段。下面是上一章的一些结构递归的例子,现在用方程编译器来定义:

open Nat def add : Nat Nat Nat | m, zero => m | m, succ n => succ (add m n) theorem add_zero (m : Nat) : add m zero = m := rfl theorem add_succ (m n : Nat) : add m (succ n) = succ (add m n) := rfl theorem zero_add : n, add zero n = n | zero => rfl | succ n => congrArg succ (zero_add n) def mul : Nat Nat Nat | unused variable `n` Note: This linter can be disabled with `set_option linter.unusedVariables false`n, zero => zero | n, succ m => add (mul n m) n

zero_add 的证明清楚地表明,归纳证明实际上是 Lean 中的一种递归形式。

上面的例子表明,add 的定义方程具有定义意义,mul 也是如此。方程编译器试图确保在任何可能的情况下都是这样, 就像直接的结构归纳法一样。然而,在其他情况下,约简只在 命题 上成立,也就是说,它们是必须明确应用的方程定理。 方程编译器在内部生成这样的定理。用户不能直接使用它们;相反,simp 策略被配置为在必要时使用它们。 因此,对 zero_add 的以下两种证明都成立:

theorem zero_add : n, add zero n = n add zero zero = zero add zero zero = zero All goals completed! 🐙 n:Natadd zero n.succ = n.succ n:Natadd zero n.succ = n.succ All goals completed! 🐙

与模式匹配定义一样,结构递归或归纳的参数可能出现在冒号之前。在处理定义之前,简单地将这些参数添加到本地上下文中。 例如,加法的定义也可以写成这样:

open Nat def add (m : Nat) : Nat Nat | zero => m | succ n => succ (add m n)

你也可以用 match 来写上面的例子。

open Nat def add (m n : Nat) : Nat := match n with | zero => m | succ n => succ (add m n)

一个更有趣的结构递归的例子是斐波那契函数 fib

def fib : Nat Nat | 0 => 1 | 1 => 1 | n+2 => fib (n+1) + fib n example : fib 0 = 1 := rfl example : fib 1 = 1 := rfl example : fib (n + 2) = fib (n + 1) + fib n := rfl example : fib 7 = 21 := rfl

这里,fib 函数在 n + 2 (定义上等于 succ (succ n) )处的值是根据 n + 1 (定义上等价于 succ n )和 n 处的值定义的。然而,这是一种众所周知的计算斐波那契函数的低效方法, 其执行时间是 n 的指数级。这里有一个更好的方法:

def fibFast (n : Nat) : Nat := (loop n).2 where loop : Nat Nat × Nat | 0 => (0, 1) | n+1 => let p := loop n; (p.2, p.1 + p.2) 573147844013817084101#eval fibFast 100
573147844013817084101

这里是使用 let rec 而不是 where 的相同定义。

def fibFast (n : Nat) : Nat := let rec loop : Nat Nat × Nat | 0 => (0, 1) | n+1 => let p := loop n; (p.2, p.1 + p.2) (loop n).2

在这两种情况下,Lean 都会生成辅助函数 fibFast.loop

为了处理结构递归,方程编译器使用 值域 递归(course-of-values recursion),使用每个归纳定义类型自动生成的常量 belowbrecOn。 你可以通过查看 Nat.belowNat.brecOn 的类型来了解它是如何工作的:

variable (C : Nat Type u) Nat.below : Nat Type u#check (@Nat.below C : Nat Type u)
Nat.below : Nat  Type u
Nat.below 3#reduce @Nat.below C (3 : Nat)
Nat.below 3
Nat.brecOn : (t : Nat) ((t : Nat) Nat.below t C t) C t#check (@Nat.brecOn C : (n : Nat) ((n : Nat) @Nat.below C n C n) C n)
Nat.brecOn : (t : Nat)  ((t : Nat)  Nat.below t  C t)  C t

类型 @Nat.below C (3 : Nat) 是一个数据结构,它存储 C 0C 1C 2 的元素。 值域递归由 Nat.brecOn 实现。它使我们能够根据函数的所有先前值(表示为 @Nat.below C n 的元素) 定义类型为 (n : Nat) C n 的依赖函数在特定输入 n 处的值。

使用值域递归是方程编译器用来向 Lean 内核证明函数终止的技术之一。它不影响代码生成器,代码生成器像其他函数式编程语言编译器一样编译递归函数。 回想一下,#eval fib <n><n> 上是指数级的。 另一方面,#reduce fib <n> 是高效的,因为它使用发送到内核的基于 brecOn 构造的定义。

def fib : Nat Nat | 0 => 1 | 1 => 1 | n+2 => fib (n+1) + fib n -- Slow: -- #eval fib 50 -- Fast: 20365011074#reduce fib 50
20365011074
def fib : Nat Nat := fun x => Nat.brecOn x fun x f => (match (motive := (x : Nat) Nat.below x Nat) x with | 0 => fun x => 1 | 1 => fun x => 1 | n.succ.succ => fun x => x.1 + x.2.1) f#print fib
def fib : Nat  Nat :=
fun x =>
  Nat.brecOn x fun x f =>
    (match (motive := (x : Nat)  Nat.below x  Nat) x with
      | 0 => fun x => 1
      | 1 => fun x => 1
      | n.succ.succ => fun x => x.1 + x.2.1)
      f

递归定义的另一个很好的例子是列表 append 函数。

def append : List α List α List α | [], bs => bs | a::as, bs => a :: append as bs example : append [1, 2, 3] [4, 5] = [1, 2, 3, 4, 5] := rfl

这里还有另一个例子:它将第一个列表的元素添加到第二个列表的元素中,直到两个列表中的一个用完为止。

def listAdd [Add α] : List α List α List α | [], _ => [] | _, [] => [] | a :: as, b :: bs => (a + b) :: listAdd as bs [5, 7, 9]#eval listAdd [1, 2, 3] [4, 5, 6, 6, 9, 10]
[5, 7, 9]

鼓励你在下面的练习中尝试类似的例子。

8.4. 局部递归声明🔗

你可以使用 let rec 关键字定义局部递归声明。

def replicate (n : Nat) (a : α) : List α := let rec loop : Nat List α List α | 0, as => as | n+1, as => loop n (a::as) loop n [] @replicate.loop : {α : Type u_1} α Nat List α List α#check @replicate.loop
@replicate.loop : {α : Type u_1}  α  Nat  List α  List α

Lean 为每个 let rec 创建一个辅助声明。在上面的例子中,它为出现在 replicate 中的 let rec loop 创建了声明 replicate.loop。 注意,Lean 通过将出现在 let rec 声明中的任何局部变量添加为附加参数来“闭合”声明。例如,局部变量 a 出现在 let rec loop 中。

你也可以在策略模式下使用 let rec,以及用于创建归纳证明。

theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := α:Type u_1n:Nata:α(replicate n a).length = n let rec aux (n : Nat) (as : List α) : (replicate.loop a n as).length = n + as.length := α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a n as).length = n + as.length match n with α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a 0 as).length = 0 + as.length All goals completed! 🐙 α:Type u_1n✝¹:Nata:αn✝:Natas:List αn:Nat(replicate.loop a (n + 1) as).length = n + 1 + as.length All goals completed! 🐙 All goals completed! 🐙

你也可以在定义之后使用 where 子句引入辅助递归声明。 Lean 将它们转换为 let rec

def replicate (n : Nat) (a : α) : List α := loop n [] where loop : Nat List α List α | 0, as => as | n+1, as => loop n (a::as) theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := α:Type u_1n:Nata:α(replicate n a).length = n All goals completed! 🐙 where aux (n : Nat) (as : List α) : (replicate.loop a n as).length = n + as.length := α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a n as).length = n + as.length match n with α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a 0 as).length = 0 + as.length All goals completed! 🐙 α:Type u_1n✝¹:Nata:αn✝:Natas:List αn:Nat(replicate.loop a (n + 1) as).length = n + 1 + as.length All goals completed! 🐙

8.5. 良基递归和归纳🔗

当无法使用结构递归时,我们可以使用良基递归来证明终止。 我们需要一个良基关系,以及一个证明,证明每个递归应用相对于该关系都是递减的。 依赖类型理论足够强大,可以编码和证明良基递归。让我们从理解其工作原理所需的逻辑背景开始。

Lean 的标准库定义了两个谓词 Acc r aWellFounded r,其中 r 是类型 α 上的二元关系, a 是类型 α 的元素。

variable (α : Sort u) variable (r : α α Prop) Acc r : α Prop#check (Acc r : α Prop)
Acc r : α  Prop
WellFounded r : Prop#check (WellFounded r : Prop)
WellFounded r : Prop

第一个 Acc 是一个归纳定义的谓词。根据其定义,Acc r x 等价于 y, r y x Acc r y。 如果你认为 r y x 表示一种顺序关系 y x,那么 Acc r x 表示 x 是从下方可访问的, 即它的所有前驱都是可访问的。特别是,如果 x 没有前驱,它是可访问的。给定任何类型 α, 我们应该能够递归地为 α 的每个可访问元素赋值,方法是首先为其所有前驱赋值。

关于 r 是良基的陈述,记为 WellFounded r,正是该类型的每个元素都是可访问的陈述。 根据上述考虑,如果 r 是类型 α 上的良基关系,我们应该在 α 上有一个关于关系 r 的良基递归原理。 确实,我们有:标准库定义了 WellFounded.fix,它正是用于此目的。

noncomputable def f {α : Sort u} (r : α α Prop) (h : WellFounded r) (C : α Sort v) (F : (x : α) ((y : α) r y x C y) C x) : (x : α) C x := WellFounded.fix h F

这里有很多角色,但我们已经看到了第一块:类型 α,关系 r,以及假设 h,即 r 是良基的。 变量 C 代表递归定义的动机:对于每个元素 x : α,我们想要构造一个 C x 的元素。 函数 F 提供了这样做的归纳配方:它告诉我们如何构造一个元素 C x,给定 x 的每个前驱 yC y 元素。

注意,WellFounded.fix 同样可以作为归纳原理。它说如果 是良基的,并且你想证明 x, C x, 只要证明对于任意 x,如果我们有 y, r y x C y,那么我们有 C x 就足够了。

在上面的例子中,我们使用修饰符 noncomputable,因为代码生成器目前不支持 WellFounded.fix。 函数 WellFounded.fix 是 Lean 用来证明函数终止的另一个工具。

Lean 知道自然数上的通常顺序 < 是良基的。它还知道许多从其他良基顺序构造新良基顺序的方法,例如使用字典序。

这本质上是标准库中自然数除法的定义。

open Nat theorem div_lemma {x y : Nat} : 0 < y y x x - y < x := fun h => sub_lt (Nat.lt_of_lt_of_le h.left h.right) h.left def div.F (x : Nat) (f : (x₁ : Nat) x₁ < x Nat Nat) (y : Nat) : Nat := if h : 0 < y y x then f (x - y) (div_lemma h) y + 1 else zero noncomputable def div := WellFounded.fix (measure id).wf div.F 4#reduce div 8 2
4

繁饰器(Elaborator)可以使这样的定义更加方便。它接受下列内容:

def div (x y : Nat) : Nat := if h : 0 < y y x then have : x - y < x := Nat.sub_lt (Nat.lt_of_lt_of_le h.1 h.2) h.1 div (x - y) y + 1 else 0

当 Lean 遇到递归定义时,它首先尝试结构递归,失败时才返回到良基递归。Lean 使用 decreasing_tactic 来显示递归应用会越来越小。 上面例子中的辅助命题 x - y < x 应该被视为这种策略的提示。

div 的定义公式不具有定义性,但我们可以使用 unfold 策略展开 div。 我们使用 conv 来选择要展开的 div 应用。

example (x y : Nat) : div x y = if 0 < y y x then div (x - y) y + 1 else 0 := x:Naty:Natdiv x y = if 0 < y y x then div (x - y) y + 1 else 0 -- unfold occurrence in the left-hand-side of the equation: x:Naty:Nat| div x y = if 0 < y y x then div (x - y) y + 1 else 0 x:Naty:Nat| div x y; x:Naty:Nat| if h : 0 < y y x then have this := ; div (x - y) y + 1 else 0 All goals completed! 🐙 example (x y : Nat) (h : 0 < y y x) : div x y = div (x - y) y + 1 := x:Naty:Nath:0 < y y xdiv x y = div (x - y) y + 1 x:Naty:Nath:0 < y y x| div x y = div (x - y) y + 1 x:Naty:Nath:0 < y y x| div x y; x:Naty:Nath:0 < y y x| if h : 0 < y y x then have this := ; div (x - y) y + 1 else 0 All goals completed! 🐙

下面的示例与此类似:它将任何自然数转换为二进制表达式,表示为 0 和 1 的列表。我们必须提供递归调用正在递减的证据, 我们在这里用 sorry 来做。sorry 并不会阻止解释器成功地对函数求值, 但是当一个项包含 sorry 时,必须使用 #eval! 而不是 #eval

def declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'declaration uses 'sorry'natToBin : Nat List Nat | 0 => [0] | 1 => [1] | n + 2 => have : (n + 2) / 2 < n + 2 := sorry natToBin ((n + 2) / 2) ++ [n % 2] [1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1]#eval! natToBin 1234567

最后一个例子,我们观察到 Ackermann 函数可以直接定义,因为它可以被自然数上字典序的良基性证明。 termination_by 子句指示 Lean 使用字典序。这个子句实际上是将函数参数映射到类型为 Nat × Nat 的元素。 然后,Lean 使用类型类解析来合成类型为 WellFoundedRelation (Nat × Nat) 的元素。

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) termination_by x y => (x, y)

在许多情况下,Lean 可以自动确定适当的字典序。Ackermann 函数就是这样一种情况,因此 termination_by 子句是可选的:

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y)

注意,在上面的例子中使用了字典序,因为实例 WellFoundedRelation (α × β) 使用了字典序。Lean 还定义了实例

instance (priority := low) [SizeOf α] : WellFoundedRelation α := sizeOfWFRel

在下面的例子中,我们通过显示 as.size - i 在递归应用中是递减的来证明它会终止。

def takeWhile (p : α Bool) (as : Array α) : Array α := go 0 #[] where go (i : Nat) (r : Array α) : Array α := if h : i < as.size then let a := as[i] if p a then go (i+1) (r.push a) else r else r termination_by as.size - i

注意,辅助函数 go 在这个例子中是递归的,但 takeWhile 不是。 Lean 再次可以自动识别这种模式,因此 termination_by 子句是不必要的:

def takeWhile (p : α Bool) (as : Array α) : Array α := go 0 #[] where go (i : Nat) (r : Array α) : Array α := if h : i < as.size then let a := as[i] if p a then go (i+1) (r.push a) else r else r

默认情况下,Lean 使用 decreasing_tactic 来证明递归应用正在递减。修饰词 decreasing_by 允许我们提供自己的策略。这里有一个例子。

theorem div_lemma {x y : Nat} : 0 < y y x x - y < x := fun ypos, ylex => Nat.sub_lt (Nat.lt_of_lt_of_le ypos ylex) ypos def div (x y : Nat) : Nat := if h : 0 < y y x then div (x - y) y + 1 else 0 decreasing_by y:Natx:Nath:0 < y y x0 < y y x; All goals completed! 🐙

注意 decreasing_by 不是 termination_by 的替代品,它们是互补的。termination_by 用于指定良基关系, 而 decreasing_by 用于提供我们自己的策略来显示递归应用正在递减。在下面的例子中,我们同时使用了它们。

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) termination_by x y => (x, y) decreasing_by -- unfolds well-founded recursion auxiliary definitions: all_goals x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ NatProd.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x, x✝ x + 1, y ) (x + 1, y + 1) x:NatProd.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x, 1) (x + 1, 0) x:Natx < x + 1; All goals completed! 🐙 x:Naty:NatProd.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x + 1, y) (x + 1, y + 1) x:Naty:Naty < y + 1; All goals completed! 🐙 x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ NatProd.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x, x✝ x + 1, y ) (x + 1, y + 1) x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ Natx < x + 1; All goals completed! 🐙

我们可以使用 decreasing_by sorry 来指示 Lean “相信”我们函数会终止。

def declaration uses 'sorry'natToBin : Nat List Nat | 0 => [0] | 1 => [1] | n + 2 => natToBin ((n + 2) / 2) ++ [n % 2] decreasing_by All goals completed! 🐙 [1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1]#eval! natToBin 1234567

回想一下,使用 sorry 等同于使用一个新的公理,应该避免。在下面的例子中,我们使用 sorry 来证明 False。 命令 #print axioms unsound 显示 unsound 依赖于用于实现 sorry 的不安全公理 sorryAx

def declaration uses 'sorry'unsound (x : Nat) : False := unsound (x + 1) decreasing_by All goals completed! 🐙 unsound 0 : False#check unsound 0
unsound 0 : False
-- `unsound 0` is a proof of `False` 'unsound' depends on axioms: [sorryAx]#print axioms unsound
'unsound' depends on axioms: [sorryAx]

总结:

  • 如果没有 termination_by,则通过选择一个参数,然后使用类型类解析为该参数的类型合成一个良基关系,从而推导出一个良基关系(如果可能的话)。

  • 如果指定了 termination_by,它将函数的参数映射到类型 α,并再次使用类型类解析。回想一下,β × γ 的默认实例是基于 βγ 的良基关系的字典序。

  • Nat 的默认良基关系实例是 (· < ·)

  • 默认情况下,策略 decreasing_tactic 用于显示递归应用相对于所选良基关系是更小的。如果 decreasing_tactic 失败,错误消息将包含剩余的目标 ... |- G。注意,decreasing_tactic 使用 assumption。因此,你可以包含一个 have 表达式来证明目标 G。你也可以使用 decreasing_by 提供你自己的策略。

8.6. 函数归纳🔗

Lean 为递归函数生成定制的归纳原理。这些归纳原理遵循函数定义的递归结构,而不是数据类型的结构。 关于函数的证明通常遵循函数本身的递归结构,因此这些归纳原理允许更方便地证明关于函数的陈述。

例如,使用 ack 的函数归纳原理来证明结果总是大于 0,需要为 ack 中的每个模式匹配分支提供一个案例:

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) theorem ack_gt_zero : ack n m > 0 := n:Natm:Natack n m > 0 fun_induction ack with y:Naty + 1 > 0 All goals completed! 🐙 x:Natih:ack x 1 > 0ack x 1 > 0 All goals completed! 🐙 x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0 All goals completed! 🐙

case1y:Naty + 1 > 0 中,目标是:

y:Naty + 1 > 0

目标中的 y + 1 对应于 ack 第一种情况返回的值。

case2x:Natih:ack x 1 > 0ack x 1 > 0 中,目标是:

x:Natih:ack x 1 > 0ack x 1 > 0

目标中的 ack x 1 对应于 ack 应用于 ack 第二种情况返回的模式变量 x + 10 的值。 该项自动简化为右侧。令人高兴的是,归纳假设 ih : ack x 1 > 0 对应于递归调用,这正是这种情况下返回的答案。

case3x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0 中,目标是:

x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0

目标中的 ack x (ack (x + 1) y) 对应于 ack 第三种情况返回的值,当 ack 应用于 x + 1y + 1 时已约简。 归纳假设 ih1 : ack (x + 1) y > 0ih2 : ack x (ack (x + 1) y) > 0 对应于递归调用,其中 ih1 匹配嵌套递归调用。 再一次,归纳假设是合适的。

使用 fun_induction ack 会产生与 ack 的递归结构相匹配的目标和归纳假设。结果,证明可以只有一行:

theorem ack_gt_zero : ack n m > 0 := n:Natm:Natack n m > 0 y✝:Naty✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0ack x✝ (ack (x✝ + 1) y✝) > 0 y✝:Naty✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0ack x✝ (ack (x✝ + 1) y✝) > 0 All goals completed! 🐙

还有一个 fun_cases 策略,类似于 cases 策略。它为函数控制流中的每个分支生成一个案例。 它和 fun_induction 都额外提供了排除未采用路径的假设。

这个函数 f 表示一个五路布尔析取:

def f : Bool Bool Bool Bool Bool Bool | true, _, _, _ , _ => true | _, true, _, _ , _ => true | _, _, true, _ , _ => true | _, _, _, true, _ => true | _, _, _, _, x => x

为了证明它是析取,最后一种情况需要知道没有任何参数是 true。这个知识由策略提供:

theorem declaration uses 'sorry'f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolf b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) all_goals All goals completed! 🐙

每个案例都包含一个排除先前案例的假设:

b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5)

simp_all 策略将所有假设和目标一起简化,可以处理所有案例:

theorem f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolf b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) All goals completed! 🐙

8.7. 相互递归🔗

Lean 也支持相互递归定义。语法类似于相互归纳类型。这里有一个例子:

mutual def even : Nat Bool | 0 => true | n+1 => odd n def odd : Nat Bool | 0 => false | n+1 => even n end example : even (a + 1) = odd a := a:Nateven (a + 1) = odd a All goals completed! 🐙 example : odd (a + 1) = even a := a:Natodd (a + 1) = even a All goals completed! 🐙 theorem even_eq_not_odd : a, even a = not (odd a) := (a : Nat), even a = !odd a a:Nateven a = !odd a; even 0 = !odd 0n✝:Nata✝:even n✝ = !odd n✝even (n✝ + 1) = !odd (n✝ + 1) even 0 = !odd 0 All goals completed! 🐙 n✝:Nata✝:even n✝ = !odd n✝even (n✝ + 1) = !odd (n✝ + 1) All goals completed! 🐙

这之所以成为相互定义,是因为 even 是根据 odd 递归定义的,而 odd 是根据 even 递归定义的。 在底层,这被编译为单个递归定义。内部定义的函数接受一个和类型的元素作为参数,该元素要么是 even 的输入,要么是 odd 的输入。 然后它返回适合输入的输出。为了定义该函数,Lean 使用适当的良基度量。 内部细节旨在对用户隐藏;使用此类定义的规范方法是使用 simp(或 unfold),正如我们在上面所做的那样。

相互递归定义还提供了处理相互和嵌套归纳类型的自然方法。回想一下之前介绍的作为相互归纳谓词的 EvenOdd 的定义。

mutual inductive Even : Nat Prop where | even_zero : Even 0 | even_succ : n, Odd n Even (n + 1) inductive Odd : Nat Prop where | odd_succ : n, Even n Odd (n + 1) end

构造函数 even_zeroeven_succodd_succ 提供了证明数字是偶数或奇数的积极手段。 我们需要使用归纳类型由这些构造函数生成的事实,才能知道零不是奇数,并且后两个蕴涵是可逆的。 像往常一样,构造函数保存在以定义的类型命名的命名空间中,命令 open Even Odd 允许我们更方便地访问它们。

open Even Odd theorem not_odd_zero : ¬ Odd 0 := fun h => nomatch h theorem even_of_odd_succ : n, Odd (n + 1) Even n | _, odd_succ n h => h theorem odd_of_even_succ : n, Even (n + 1) Odd n | _, even_succ n h => h

再举一个例子,假设我们使用嵌套归纳类型来归纳定义一组项,使得一个项要么是一个常量(名称由字符串给出),要么是将一个常量应用于常量列表的结果。

inductive Term where | const : String Term | app : String List Term Term

然后我们可以使用相互递归定义来计算项中出现的常量数量,以及项列表中出现的数量。

namespace Term mutual def numConsts : Term Nat | const _ => 1 | app _ cs => numConstsLst cs def numConstsLst : List Term Nat | [] => 0 | c :: cs => numConsts c + numConstsLst cs end def sample := app "f" [app "g" [const "x"], const "y"] 2#eval numConsts sample
2
end Term

作为最后一个例子,我们定义一个函数 replaceConst a b e,它将项 e 中的常量 a 替换为 b,然后证明常量的数量是相同的。 注意,我们的证明使用了相互递归(又名归纳)。

mutual def replaceConst (a b : String) : Term Term | const c => if a == c then const b else const c | app f cs => app f (replaceConstLst a b cs) def replaceConstLst (a b : String) : List Term List Term | [] => [] | c :: cs => replaceConst a b c :: replaceConstLst a b cs end mutual theorem numConsts_replaceConst (a b : String) (e : Term) : numConsts (replaceConst a b e) = numConsts e := a:Stringb:Stringe:Term(replaceConst a b e).numConsts = e.numConsts match e with a:Stringb:Stringe:Termc:String(replaceConst a b (const c)).numConsts = (const c).numConsts a:Stringb:Stringe:Termc:String(if a = c then const b else const c).numConsts = (const c).numConsts; a:Stringb:Stringe:Termc:Stringh✝:a = c(const b).numConsts = (const c).numConstsa:Stringb:Stringe:Termc:Stringh✝:¬a = c(const c).numConsts = (const c).numConsts a:Stringb:Stringe:Termc:Stringh✝:a = c(const b).numConsts = (const c).numConstsa:Stringb:Stringe:Termc:Stringh✝:¬a = c(const c).numConsts = (const c).numConsts All goals completed! 🐙 a:Stringb:Stringe:Termf:Stringcs:List Term(replaceConst a b (app f cs)).numConsts = (app f cs).numConsts All goals completed! 🐙 theorem numConsts_replaceConstLst (a b : String) (es : List Term) : numConstsLst (replaceConstLst a b es) = numConstsLst es := a:Stringb:Stringes:List TermnumConstsLst (replaceConstLst a b es) = numConstsLst es match es with a:Stringb:Stringes:List TermnumConstsLst (replaceConstLst a b []) = numConstsLst [] All goals completed! 🐙 a:Stringb:Stringes:List Termc:Termcs:List TermnumConstsLst (replaceConstLst a b (c :: cs)) = numConstsLst (c :: cs) All goals completed! 🐙 end

8.8. 依赖模式匹配🔗

我们在 模式匹配 一节中考虑的所有模式匹配示例都可以很容易地使用 casesOnrecOn 编写。 然而,对于像 Vect α n 这样的索引归纳族,情况通常并非如此,因为案例拆分会对索引的值施加约束。 如果没有方程编译器,我们需要大量的样板代码来使用递归器定义非常简单的函数,如 mapzipunzip。 为了理解其中的困难,考虑定义一个函数 tail 需要什么,该函数接受一个向量 v : Vect α (n + 1) 并删除第一个元素。

inductive Vect (α : Type u) : Nat Type u | nil : Vect α 0 | cons : α {n : Nat} Vect α n Vect α (n + 1)

首先想到的可能是使用 Vect.casesOn 函数:

Vect.casesOn.{u, v} {α : Type v} {motive : (a : Nat) Vect α a Sort u} {a : Nat} (t : Vect α a) (nil : motive 0 nil) (cons : (a : α) {n : Nat} (a_1 : Vect α n) motive (n + 1) (cons a a_1)) : motive a t

但是在 nil 这种情况下我们应该返回什么值呢?有些奇怪的事情正在发生:如果 v 的类型是 Vect α (n + 1), 它 不能nil,但不清楚如何告诉 Vect.casesOn 这一点。

一种解决方案是定义一个辅助函数:

def tailAux (v : Vect α m) : m = n + 1 Vect α n := Vect.casesOn (motive := fun x _ => x = n + 1 Vect α n) v (fun h : 0 = n + 1 => Nat.noConfusion h) (fun (a : α) (m : Nat) (as : Vect α m) => fun (h : m + 1 = n + 1) => Nat.noConfusion h (fun h1 : m = n => h1 as)) def tail (v : Vect α (n+1)) : Vect α n := tailAux v rfl

nil 这种情况下,m 被实例化为 0Nat.noConfusion 利用了 0 = n + 1 不可能发生的事实。 否则,v 的形式为 cons a as,我们可以简单地返回 as, 在将其从长度为 m 的向量转换为长度为 n 的向量之后。

定义 tail 的困难在于维护索引之间的关系。tailAux 中的假设 m = n + 1 用于传达 n 与次要前提关联的索引之间的关系。 此外,0 = n + 1 的情况是不可达的,丢弃这种情况的规范方法是使用 Nat.noConfusion

然而,tail 函数很容易使用递归方程定义,方程编译器会自动为我们生成所有样板代码。这里有许多类似的例子:

def head : {n : Nat} Vect α (n+1) α | n, cons a as => a def tail : {n : Nat} Vect α (n+1) Vect α n | n, cons a as => as theorem eta : {n : Nat} (v : Vect α (n+1)), cons (head v) (tail v) = v | n, cons a as => rfl def map (f : α β γ) : {n : Nat} Vect α n Vect β n Vect γ n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (f a b) (map f as bs) def zip : {n : Nat} Vect α n Vect β n Vect (α × β) n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (a, b) (zip as bs)

注意,我们可以省略“不可达”情况(如 head nil)的递归方程。索引族的自动生成定义远非直截了当。例如:

def zipWith (f : α β γ) : {n : Nat} Vect α n Vect β n Vect γ n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (f a b) (zipWith f as bs) def Vect.zipWith.{u_1, u_2, u_3} : {α : Type u_1} {β : Type u_2} {γ : Type u_3} (α β γ) {n : Nat} Vect α n Vect β n Vect γ n := fun {α} {β} {γ} f x x_1 x_2 => Vect.brecOn (motive := fun x x_3 => Vect β x Vect γ x) x_1 (fun x x_3 f_1 x_4 => (match (motive := (x : Nat) (x_5 : Vect α x) Vect β x Vect.below (motive := fun x x_7 => Vect β x Vect γ x) x_5 Vect γ x) x, x_3, x_4 with | 0, nil, nil => fun x => nil | n.succ, cons a as, cons b bs => fun x => cons (f a b) (x.1 bs)) f_1) x_2#print zipWith
def Vect.zipWith.{u_1, u_2, u_3} : {α : Type u_1} 
  {β : Type u_2}  {γ : Type u_3}  (α  β  γ)  {n : Nat}  Vect α n  Vect β n  Vect γ n :=
fun {α} {β} {γ} f x x_1 x_2 =>
  Vect.brecOn (motive := fun x x_3 => Vect β x  Vect γ x) x_1
    (fun x x_3 f_1 x_4 =>
      (match (motive :=
          (x : Nat) 
            (x_5 : Vect α x)  Vect β x  Vect.below (motive := fun x x_7 => Vect β x  Vect γ x) x_5  Vect γ x)
          x, x_3, x_4 with
        | 0, nil, nil => fun x => nil
        | n.succ, cons a as, cons b bs => fun x => cons (f a b) (x.1 bs))
        f_1)
    x_2
def Vect.zipWith.match_1.{u_1, u_2, u_3} : {α : Type u_1} {β : Type u_2} (motive : (x : Nat) Vect α x Vect β x Sort u_3) (x : Nat) (x_1 : Vect α x) (x_2 : Vect β x) (Unit motive 0 nil nil) ((n : Nat) (a : α) (as : Vect α n) (b : β) (bs : Vect β n) motive n.succ (cons a as) (cons b bs)) motive x x_1 x_2 := fun {α} {β} motive x x_1 x_2 h_1 h_2 => Nat.casesOn (motive := fun x => (x_3 : Vect α x) (x_4 : Vect β x) motive x x_3 x_4) x (fun x x_3 => casesOn (motive := fun a x_4 => Nat.zero = a x x_4 motive Nat.zero x x_3) x (fun h h_3 => casesOn (motive := fun a x => Nat.zero = a x_3 x motive Nat.zero nil x_3) x_3 (fun h h_4 => h_1 ()) (fun a {n} a_1 h => False.elim ) ) (fun a {n} a_1 h => False.elim ) ) (fun n x x_3 => casesOn (motive := fun a x_4 => n.succ = a x x_4 motive n.succ x x_3) x (fun h => False.elim ) (fun a {n_1} a_1 h => n.elimOffset n_1 1 h fun x_4 => Eq.ndrec (motive := fun {n_2} => (a_2 : Vect α n_2) x cons a a_2 motive n.succ x x_3) (fun a_2 h => casesOn (motive := fun a_3 x => n.succ = a_3 x_3 x motive n.succ (cons a a_2) x_3) x_3 (fun h => False.elim ) (fun a_3 {n_2} a_4 h => n.elimOffset n_2 1 h fun x => Eq.ndrec (motive := fun {n_3} => (a_5 : Vect β n_3) x_3 cons a_3 a_5 motive n.succ (cons a a_2) x_3) (fun a_5 h => h_2 n a a_2 a_3 a_5) x a_4) ) x_4 a_1) ) x_1 x_2#print zipWith.match_1
def Vect.zipWith.match_1.{u_1, u_2, u_3} : {α : Type u_1} 
  {β : Type u_2} 
    (motive : (x : Nat)  Vect α x  Vect β x  Sort u_3) 
      (x : Nat) 
        (x_1 : Vect α x) 
          (x_2 : Vect β x) 
            (Unit  motive 0 nil nil) 
              ((n : Nat) 
                  (a : α)  (as : Vect α n)  (b : β)  (bs : Vect β n)  motive n.succ (cons a as) (cons b bs)) 
                motive x x_1 x_2 :=
fun {α} {β} motive x x_1 x_2 h_1 h_2 =>
  Nat.casesOn (motive := fun x => (x_3 : Vect α x)  (x_4 : Vect β x)  motive x x_3 x_4) x
    (fun x x_3 =>
      casesOn (motive := fun a x_4 => Nat.zero = a  x  x_4  motive Nat.zero x x_3) x
        (fun h h_3 =>
           
            casesOn (motive := fun a x => Nat.zero = a  x_3  x  motive Nat.zero nil x_3) x_3
              (fun h h_4 =>   h_1 ()) (fun a {n} a_1 h => False.elim )  )
        (fun a {n} a_1 h => False.elim )  )
    (fun n x x_3 =>
      casesOn (motive := fun a x_4 => n.succ = a  x  x_4  motive n.succ x x_3) x (fun h => False.elim )
        (fun a {n_1} a_1 h =>
          n.elimOffset n_1 1 h fun x_4 =>
            Eq.ndrec (motive := fun {n_2} => (a_2 : Vect α n_2)  x  cons a a_2  motive n.succ x x_3)
              (fun a_2 h =>
                 
                  casesOn (motive := fun a_3 x => n.succ = a_3  x_3  x  motive n.succ (cons a a_2) x_3) x_3
                    (fun h => False.elim )
                    (fun a_3 {n_2} a_4 h =>
                      n.elimOffset n_2 1 h fun x =>
                        Eq.ndrec (motive := fun {n_3} =>
                          (a_5 : Vect β n_3)  x_3  cons a_3 a_5  motive n.succ (cons a a_2) x_3)
                          (fun a_5 h =>   h_2 n a a_2 a_3 a_5) x a_4)
                     )
              x_4 a_1)
         )
    x_1 x_2

zipWith 函数比 tail 函数更难手动定义。我们鼓励你尝试使用 Vect.recOnVect.casesOnVect.noConfusion 来定义它。

8.9. 不可访问模式🔗

有时,依赖匹配模式中的参数对定义不是必不可少的,但仍然必须包含在内以适当地专门化表达式的类型。 Lean 允许用户将此类子项标记为 不可访问 以进行模式匹配。 例如,当出现在左侧的项既不是变量也不是构造函数应用时,这些注释是必不可少的,因为这些不是模式匹配的合适目标。 我们可以将此类不可访问模式视为模式的“不关心”组件。 你可以通过编写 .(t) 将子项声明为不可访问。如果可以推断出不可访问模式,你也可以写 _

在下面的例子中,我们声明了一个归纳类型,它定义了“在 f 的像中”的属性。 你可以将类型 ImageOf f b 的元素视为 bf 的像中的证据, 其中构造函数 imf 用于构建此类证据。然后我们可以定义任何具有“逆”的函数 f, 该逆函数将 f 像中的任何内容映射到映射到它的元素。 类型规则强制我们为第一个参数编写 f a,但该项既不是变量也不是构造函数应用, 并且在模式匹配定义中不起作用。为了定义下面的函数 inverse,我们 必须f a 标记为不可访问。

inductive ImageOf {α β : Type u} (f : α β) : β Type u where | imf : (a : α) ImageOf f (f a) open ImageOf def inverse {f : α β} : (b : β) ImageOf f b α | .(f a), imf a => a def inverse' {f : α β} : (b : β) ImageOf f b α | _, imf a => a

在上面的例子中,不可访问注释清楚地表明 f 不是 模式匹配变量。

不可访问模式可用于阐明和控制使用依赖模式匹配的定义。考虑函数 Vect.add 的以下定义, 它将某种类型的元素的两个向量相加,假设该类型具有关联的加法函数:

inductive Vect (α : Type u) : Nat Type u | nil : Vect α 0 | cons : α {n : Nat} Vect α n Vect α (n+1) def Vect.add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | 0, nil, nil => nil | unused variable `n` Note: This linter can be disabled with `set_option linter.unusedVariables false`n+1, cons a as, cons b bs => cons (a + b) (add as bs)

参数 {n : Nat} 出现在冒号之后,因为它不能在整个定义中保持固定。在实现此定义时, 方程编译器首先区分第一个参数是 0 还是 n+1 的形式。 接下来是对接下来的两个参数进行嵌套案例拆分,并且在每种情况下,方程编译器都会排除与第一个模式不兼容的情况。

但实际上,第一个参数不需要案例拆分;当我们对第二个参数进行案例拆分时,VectcasesOn 消除器会自动抽象此参数并将其替换为 0n + 1。 使用不可访问模式,我们可以提示方程编译器避免对 n 进行案例拆分。

def add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | .(_), nil, nil => nil | .(_), cons a as, cons b bs => cons (a + b) (add as bs)

将位置标记为不可访问模式首先告诉方程编译器,参数的形式应该从其他参数施加的约束中推断出来, 其次,第一个参数 应该参与模式匹配。

为了方便起见,不可访问模式 .(_) 可以写成 _

def add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | _, nil, nil => nil | _, cons a as, cons b bs => cons (a + b) (add as bs)

正如我们上面提到的,参数 {n : Nat} 是模式匹配的一部分,因为它不能在整个定义中保持固定。 Lean 不要求显式提供这些判别式,而是自动为我们隐式包含这些额外的判别式。

def add [Add α] {n : Nat} : Vect α n Vect α n Vect α n | nil, nil => nil | cons a as, cons b bs => cons (a + b) (add as bs)

当与 自动绑定隐式 功能结合使用时,你可以进一步简化声明并编写:

def add [Add α] : Vect α n Vect α n Vect α n | nil, nil => nil | cons a as, cons b bs => cons (a + b) (add as bs)

使用这些新功能,你可以更紧凑地编写前面部分中定义的其他向量函数,如下所示:

def head : Vect α (n+1) α | cons a as => a def tail : Vect α (n+1) Vect α n | cons a as => as theorem eta : (v : Vect α (n+1)) cons (head v) (tail v) = v | cons a as => rfl def map (f : α β γ) : Vect α n Vect β n Vect γ n | nil, nil => nil | cons a as, cons b bs => cons (f a b) (map f as bs) def zip : Vect α n Vect β n Vect (α × β) n | nil, nil => nil | cons a as, cons b bs => cons (a, b) (zip as bs)

8.10. 匹配表达式🔗

Lean 还为许多函数式语言中常见的 match-with 表达式提供了编译器:

def isNotZero (m : Nat) : Bool := match m with | 0 => false | n + 1 => true

这看起来与普通的模式匹配定义没有太大区别,但重点是 match 可以在表达式的任何地方使用,并且可以使用任意参数。

def isNotZero (m : Nat) : Bool := match m with | 0 => false | n + 1 => true def filter (p : α Bool) : List α List α | [] => [] | a :: as => match p a with | true => a :: filter p as | false => filter p as example : filter isNotZero [1, 0, 0, 3, 0] = [1, 3] := rfl

这里有另一个例子:

def foo (n : Nat) (b c : Bool) := 5 + match n - 5, b && c with | 0, true => 0 | m + 1, true => m + 7 | 0, false => 5 | m + 1, false => m + 3 9#eval foo 7 true false
9
example : foo 7 true false = 9 := rfl

Lean 在内部使用 match 构造来实现系统所有部分的模式匹配。因此,这四个定义具有相同的净效果:

def bar₁ : Nat × Nat Nat | (m, n) => m + n def bar₂ (p : Nat × Nat) : Nat := match p with | (m, n) => m + n def bar₃ : Nat × Nat Nat := fun (m, n) => m + n def bar₄ (p : Nat × Nat) : Nat := let (m, n) := p; m + n

这些变体对于解构命题同样有用:

variable (p q : Nat Prop) example : ( x, p x) ( y, q y) x y, p x q y | x, px, y, qy => x, y, px, qy example (h₀ : x, p x) (h₁ : y, q y) : x y, p x q y := match h₀, h₁ with | x, px, y, qy => x, y, px, qy example : ( x, p x) ( y, q y) x y, p x q y := fun x, px y, qy => x, y, px, qy example (h₀ : x, p x) (h₁ : y, q y) : x y, p x q y := let x, px := h₀ let y, qy := h₁ x, y, px, qy

8.11. 练习🔗

  1. 打开一个命名空间 Hidden 以避免命名冲突,并使用方程编译器定义自然数上的加法、乘法和幂运算。 然后使用方程编译器推导它们的一些基本属性。

  1. 类似地,使用方程编译器定义列表上的一些基本操作(如 reverse 函数),并通过归纳法证明关于列表的定理(例如对于任何列表 xsreverse (reverse xs) = xs)。

  1. 定义你自己的函数来对自然数执行值域递归。同样,看看你是否能弄清楚如何自己定义 WellFounded.fix

  1. 按照 依赖模式匹配 一节中的示例,定义一个将两个向量连接起来的函数。 这很棘手;你必须定义一个辅助函数。

  2. 考虑以下算术表达式类型。其思想是 var n 是一个变量 vₙ,而 const n 是值为 n 的常数。

    inductive Expr where | const : Nat Expr | var : Nat Expr | plus : Expr Expr Expr | times : Expr Expr Expr deriving Repr open Expr def sampleExpr : Expr := plus (times (var 0) (const 7)) (times (const 2) (var 1))

    -- Here sampleExpr represents (v₀ * 7) + (2 * v₁).

    这里 sampleExpr 表示 (v₀ * 7) + (2 * v₁)

    编写一个函数来计算这样的表达式,将每个 var n 计算为 v n

    def declaration uses 'sorry'eval (v : Nat Nat) : Expr Nat | const n => sorry | var n => v n | plus e₁ e₂ => sorry | times e₁ e₂ => sorry def sampleVal : Nat Nat | 0 => 5 | 1 => 6 | _ => 0 -- 试一试。你应该在这里得到 47。 -- #eval eval sampleVal sampleExpr

    实现“常量融合”,这是一个将子项(如 5 + 7)简化为 12 的过程。使用辅助函数 simpConst,定义一个函数“fuse”:为了简化加法或乘法,首先递归地简化参数,然后应用 simpConst 尝试简化结果。

    def simpConst : Expr Expr | plus (const n₁) (const n₂) => const (n₁ + n₂) | times (const n₁) (const n₂) => const (n₁ * n₂) | e => e def declaration uses 'sorry'fuse : Expr Expr := sorry theorem declaration uses 'sorry'simpConst_eq (v : Nat Nat) : e : Expr, eval v (simpConst e) = eval v e := sorry theorem declaration uses 'sorry'fuse_eq (v : Nat Nat) : e : Expr, eval v (fuse e) = eval v e := sorry

    最后两个定理表明这些定义保留了值。