8. 归纳与递归
在前一章中,我们看到归纳定义(inductive definition)为在 Lean 中引入新类型(type)提供了一种强大的手段。此外,构造子和递归子(recursor)是在这些类型上定义函数的唯一方法。通过命题即类型(propositions-as-types)对应,这意味着归纳(induction)是基本的证明方法。
Lean 提供了定义递归函数(recursive function)、执行模式匹配(pattern matching)和编写归纳证明的自然方式。它允许你通过指定函数应满足的方程来定义函数,并允许你通过指定如何处理可能出现的各种情况来证明定理。在幕后,这些描述被"编译"为原始的递归子,使用一个我们称为"方程编译器(equation compiler)"的过程。方程编译器不属于可信代码基;它的输出包含由内核独立检查的项(term)。
8.1. 模式匹配
模式图(schematic pattern)的解释是编译过程的第一步。我们已经看到,casesOn 递归子可以根据归纳定义类型(inductive type)中涉及的构造子,按情况用于定义函数和证明定理。但复杂的定义可能使用多个嵌套的 casesOn 应用,并且难以阅读和理解。模式匹配提供了一种更方便的方法,并且为函数式编程语言的用户所熟悉。
考虑自然数的归纳定义类型。每个自然数要么是 zero,要么是 succ x,因此你可以通过指定每种情况下的值来定义从自然数到任意类型的函数:
set_option linter.unusedVariables false
open Nat
def sub1 : Nat → Nat
| zero => zero
| succ x => x
def isZero : Nat → Bool
| zero => true
| succ x => false
用于定义这些函数的方程在定义上成立(hold definitionally):
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
set_option linter.unusedVariables false
def sub1 : Nat → Nat
| 0 => 0
| x + 1 => x
def isZero : Nat → Bool
| 0 => true
| x + 1 => false
由于加法和零符号已被赋予 [match_pattern] 属性,它们可以在模式匹配中使用。Lean 只是将这些表达式归一化(normalize),直到构造子 zero 和 succ 被暴露出来。
模式匹配适用于任何归纳类型,例如积类型(product)和选项类型(option type):
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
这里我们不仅用它来定义函数,还用来按情况执行证明:
namespace Hidden
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
end Hidden
模式匹配也可以用于析构(destruct)归纳定义的命题(proposition):
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
这提供了一种紧凑的方式来解包(unpack)使用逻辑连接词(logical connective)的假设。
在所有这些例子中,模式匹配用于执行单个情况区分。更有趣的是,模式可以包含嵌套的构造子,如下面的例子所示。
方程编译器首先根据输入是 zero 还是形如 succ x 进行情况分割。然后它继续对 x 是 zero 还是 succ x 进行情况分割。它从提供给它的模式中确定必要的情况分割,如果模式未能穷尽所有情况,则引发错误。同样,我们可以使用算术符号,如以下版本所示。无论哪种情况,定义方程都在定义上成立。
example : sub2 0 = 0 := rfl
example : sub2 1 = 0 := rfl
example : sub2 (x+2) = x := rfl
example : sub2 5 = 3 := rfl
你可以写 #print sub2 来看看该函数是如何被编译成递归子的。(Lean 会告诉你 sub2 是根据一个内部辅助函数 sub2.match_1 定义的,但你也可以打印出那个函数。)Lean 使用这些辅助函数来编译 match 表达式。实际上,上面的定义会被展开为
以下是一些嵌套模式匹配的更多例子:
set_option linter.unusedVariables false
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
方程编译器可以顺序地处理多个参数。例如,将前面的例子定义为两个参数的函数会更自然:
set_option linter.unusedVariables false
def foo : Nat → Nat → Nat
| 0, n => 0
| m + 1, 0 => 1
| m + 1, n + 1 => 2
这是另一个例子:
set_option linter.unusedVariables false
def bar : List Nat → List Nat → Nat
| [], [] => 0
| a :: as, [] => a
| [], b :: bs => b
| a :: as, b :: bs => a + b
注意,模式之间用逗号分隔。
在以下每个例子中,即使其他参数也包含在模式列表中,分割也只发生在第一个参数上。
set_option linter.unusedVariables false
namespace Hidden
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
end Hidden
还要注意,当某个参数的值在定义中不需要时,你可以使用下划线代替。这个下划线被称为通配符模式(wildcard pattern),或匿名变量(anonymous variable)。与方程编译器外部不同,这里的下划线并不表示隐式参数(implicit argument)。在函数式编程语言中,使用下划线作为通配符是很常见的,因此 Lean 采用了这种符号。关于通配符和重叠模式的部分进一步阐述了通配符的概念,而不可访问模式的描述解释了如何在模式中使用隐式参数。
如归纳类型中所述,归纳数据类型可以依赖于参数。下面的例子使用模式匹配定义了 tail 函数。参数 α : Type u 是一个参数,出现在冒号之前,表示它不参与模式匹配。Lean 也允许参数出现在 : 之后,但在它们上进行模式匹配需要显式的 match。
set_option linter.unusedVariables false
def tail1 {α : Type u} : List α → List α
| [] => []
| a :: as => as
def tail2 : {α : Type u} → List α → List α
| α, [] => []
| α, a :: as => as
尽管参数 α 在这两个例子中的位置不同,但在两种情况下它都以相同的方式处理,即不参与情况分割。
Lean 还可以处理更复杂的模式匹配形式,其中依赖类型(dependent type)的参数对不同的情况施加了额外的约束。这种依值模式匹配(dependent pattern matching)的例子将在依值模式匹配一节中讨论。
8.2. 通配符与重叠模式
考虑上一节中的一个例子:
set_option linter.unusedVariables false
def foo : Nat → Nat → Nat
| 0, n => 0
| m + 1, 0 => 1
| m + 1, n + 1 => 2
另一种表示方式是:
set_option linter.unusedVariables false
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
由于 m 和 n 的值不需要,我们完全可以使用通配符模式。
你可以验证这个 foo 的定义满足与之前相同的定义恒等式。
一些函数式编程语言支持不完全模式(incomplete pattern)。在这些语言中,解释器会对不完全的情况产生异常或返回一个任意值。我们可以使用 Inhabited 类型类(type class)来模拟任意值的方法。大致来说,Inhabited α 的一个元素是 α 中存在一个元素的见证;在关于类型类的章节中,我们将看到可以指示 Lean 适当的基本类型是 inhabited 的,并且可以自动推断其他构造的类型是 inhabited 的。在此基础上,标准库为任何 inhabited 的类型提供了一个默认元素 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。
8.3. 结构递归与归纳
使方程编译器强大的是它还支持递归定义。在接下来的三节中,我们将分别描述:
-
结构递归(structurally recursive)定义
-
良基递归(well-founded recursive)定义
-
互递归(mutually recursive)定义
一般来说,方程编译器处理以下形式的输入:
def foo (a : α) : (b : β) → γ | [patterns₁] => t₁ ... | [patternsₙ] => tₙ
这里 (a : α) 是一个参数序列,(b : β) 是执行模式匹配的参数序列,而 γ 是任意类型,可以依赖于 a 和 b。每一行应包含相同数量的模式,每个 β 中的元素对应一个模式。如我们所见,模式要么是一个变量,要么是一个应用于其他模式的构造子,要么是一个归一化为这种形式的表达式(其中非构造子用 [match_pattern] 属性标记)。构造子的出现引发情况分割,构造子的参数由给定的变量表示。在依值模式匹配一节中,我们将看到,模式中的一些显式项被迫采用特定形式以使表达式的类型检查通过,尽管它们在模式匹配中不起作用。由于这个原因,它们被称为"不可访问模式(inaccessible patterns)"。但在介绍依值模式匹配之前,我们不需要使用这样的不可访问模式。
如上一节所述,项 t₁, ..., tₙ 可以使用任何参数 a,以及相应模式中引入的任何变量。递归和归纳之所以可能,是因为它们还可以涉及对 foo 的递归调用。在本节中,我们将处理结构递归(structural recursion),其中出现在 => 右侧的 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
| n, zero => zero
| n, succ m => add (mul n m) n
zero_add 的证明表明,归纳证明(proof by induction)在 Lean 中实际上是一种递归形式。
上面的例子表明,add 的定义方程在定义上成立,mul 也是如此。方程编译器尽可能确保这一点成立,就像直接的结构归纳(structural induction)那样。然而,在其他情况下,规约(reduction)只在命题上(propositionally)成立,也就是说,它们是必须显式应用的等式定理。方程编译器在内部生成这样的定理。它们并非供用户直接使用;相反,simp 策略(tactic)被配置为在必要时使用它们。以下 zero_add 的证明就是以这种方式工作的:
theorem zero_add : ∀ n, add zero n = n
⊢ add zero zero = zero ⊢ add zero zero = zero All goals completed! 🐙
n:Nat⊢ add zero n.succ = n.succ n:Nat⊢ add zero n.succ = n.succ All goals completed! 🐙
与通过模式匹配定义一样,结构递归或归纳的参数可以出现在冒号之前。这些参数在处理定义之前被简单地添加到本地上下文中。例如,加法的定义也可以写成如下形式:
你也可以使用 match 来编写上面的例子。
结构递归的一个更有趣的例子是斐波那契函数 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)
#eval fibFast 100以下是使用 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),使用每个归纳定义类型自动生成的常量 below 和 brecOn。你可以通过查看 Nat.below 和 Nat.brecOn 的类型来了解其工作原理:
variable (C : Nat → Type u)
#check (@Nat.below C : Nat → Type u)
#reduce @Nat.below C (3 : Nat)
#check (@Nat.brecOn C : (n : Nat) → ((n : Nat) → @Nat.below C n → C n) → C n)类型 @Nat.below C (3 : Nat) 是一个存储 C 0、C 1 和 C 2 的数据结构。值域递归由 Nat.brecOn 实现。它使我们能够根据函数的所有先前值(以 @Nat.below C n 的一个元素的形式呈现),在特定输入 n 处定义类型为 (n : Nat) → C 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:
#reduce fib 50
#print fib递归定义的另一个好例子是列表的 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
#eval listAdd [1, 2, 3] [4, 5, 6, 6, 9, 10]鼓励你在下面的练习中尝试类似的例子。
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 []
#check @replicate.loopLean 为每个 let rec 创建一个辅助声明。在上面的例子中,它为 replicate 中出现的 let rec loop 创建了声明 replicate.loop。请注意,Lean 通过将 let rec 声明中出现的任何局部变量作为附加参数添加来"闭合(close)"该声明。例如,局部变量 a 出现在 let rec loop 中。
你也可以在策略模式(tactic mode)中使用 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 []
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. 良基递归与归纳
当无法使用结构递归时,我们可以使用良基递归(well-founded recursion)来证明终止。我们需要一个良基关系(well-founded relation),以及每个递归应用相对于该关系是递减的证明。依值类型论(dependent type theory)足够强大,可以编码和证明良基递归。让我们从理解其工作原理所需的逻辑背景开始。
Lean 的标准库定义了两个谓词(predicate),Acc r a 和 WellFounded r,其中 r 是类型 α 上的一个二元关系,a 是类型 α 的一个元素。
variable (α : Sort u)
variable (r : α → α → Prop)
#check (Acc r : α → Prop)#check (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 以及 r 是良基的假设 h。变量 C 表示递归定义的动机(motive):对每个元素 x : α,我们希望构造 C x 的一个元素。函数 F 提供了实现这一点的归纳方案:它告诉我们如何在给定 x 的每个前驱 y 的 C y 元素的情况下构造 C x 的一个元素。
注意,WellFounded.fix 同样可以作为归纳原理使用。它表明,如果 ≺ 是良基的,并且你想要证明 ∀ x, C x,那么只需证明对于任意 x,如果我们有 ∀ y, r y x → C y,那么我们有 C x。
在上面的例子中,我们使用了修饰符 noncomputable,因为代码生成器目前不支持 WellFounded.fix。函数 WellFounded.fix 是 Lean 用来证明函数终止的另一个工具。
Lean 知道自然数上的通常顺序 < 是良基的。它也知道多种从其他良基序构造新良基序的方法,例如使用字典序(lexicographic order)。
以下本质上是标准库中自然数除法的定义。
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
#reduce div 8 2这个定义有些费解。这里的递归是在 x 上进行的,div.F x f : Nat → Nat 返回针对固定 x 的"除以 y"函数。你必须记住,div.F 的第二个参数——递归的方案——是一个函数,它应该为所有小于 x 的值 x₁ 返回除以 y 的函数。
精化器(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 使用策略(tactic)decreasing_tactic 来表明递归应用是更小的。上面例子中的辅助命题 x - y < x 应被视为该策略的一个提示。
div 的定义方程不在定义上成立,但我们可以使用 unfold 策略展开 div。我们使用 conv 来选择要展开的 div 应用。
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
example (x y : Nat) :
div x y =
if 0 < y ∧ y ≤ x then
div (x - y) y + 1
else 0 := x:Naty:Nat⊢ div 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 ≤ x⊢ div 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 natToBin : Nat → List Nat
| 0 => [0]
| 1 => [1]
| n + 2 =>
have : (n + 2) / 2 < n + 2 := sorry
natToBin ((n + 2) / 2) ++ [n % 2]
#eval! natToBin 1234567
作为最后一个例子,我们观察到阿克曼函数(Ackermann's function)可以直接定义,因为它由自然数上字典序的良基性保证。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 可以自动确定适当的字典序。阿克曼函数就是这样一个例子,因此 termination_by 子句是可选的:
注意,由于实例 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 ≤ x⊢ 0 < 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⟩ → Nat⊢ Prod.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x, x✝ ⟨x + 1, y⟩ ⋯) (x + 1, y + 1)
x:Nat⊢ Prod.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x, 1) (x + 1, 0) x:Nat⊢ x < x + 1; All goals completed! 🐙
x:Naty:Nat⊢ Prod.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (x + 1, y) (x + 1, y + 1) x:Naty:Nat⊢ y < 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⟩ → Nat⊢ Prod.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⟩ → Nat⊢ x < x + 1; All goals completed! 🐙
我们可以使用 decreasing_by sorry 来指示 Lean "信任" 我们该函数会终止。
def natToBin : Nat → List Nat
| 0 => [0]
| 1 => [1]
| n + 2 => natToBin ((n + 2) / 2) ++ [n % 2]
decreasing_by All goals completed! 🐙
#eval! natToBin 1234567
回想一下,使用 sorry 相当于使用一个新公理(axiom),应该避免。在下面的例子中,我们使用 sorry 来证明 False。命令 #print axioms unsound 显示 unsound 依赖于用于实现 sorry 的非可靠公理 sorryAx。
def unsound (x : Nat) : False :=
unsound (x + 1)
decreasing_by All goals completed! 🐙
#check unsound 0-- `unsound 0` is a proof of `False`
#print axioms unsound总结:
-
如果没有
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:Nat⊢ ack n m > 0
fun_induction ack with
y:Nat⊢ y + 1 > 0
All goals completed! 🐙
x:Natih:ack x 1 > 0⊢ ack x 1 > 0
All goals completed! 🐙
x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0⊢ ack x (ack (x + 1) y) > 0
All goals completed! 🐙
在 case1y:Nat⊢ y + 1 > 0 中,目标是:
目标中的 y + 1 对应于 ack 第一种情况返回的值。
在 case2x:Natih:ack x 1 > 0⊢ ack x 1 > 0 中,目标是:
目标中的 ack x 1 对应于 ack 在第二种情况下返回的、应用于模式变量 x + 1 和 0 的值。该项自动简化为右侧。令人高兴的是,归纳假设 ih : ack x 1 > 0 对应于递归调用,这正是该情况下返回的答案。
在 case3x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0⊢ ack x (ack (x + 1) y) > 0 中,目标是:
目标中的 ack x (ack (x + 1) y) 对应于 ack 在第三种情况下返回的值,即 ack 应用于 x + 1 和 y + 1 的结果。归纳假设 ih1 : ack (x + 1) y > 0 和 ih2 : ack x (ack (x + 1) y) > 0 对应于内层和外层的递归调用。同样,归纳假设是合适的。
使用 fun_induction ack 产生的目标和归纳假设与 ack 的递归结构匹配。因此,证明可以是一行:
theorem ack_gt_zero : ack n m > 0 := n:Natm:Nat⊢ ack n m > 0
y✝:Nat⊢ y✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0⊢ ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0⊢ ack x✝ (ack (x✝ + 1) y✝) > 0 y✝:Nat⊢ y✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0⊢ ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0⊢ ack 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。这个知识由策略提供:
def f : Bool → Bool → Bool → Bool → Bool → Bool
| true, _, _, _ , _ => true
| _, true, _, _ , _ => true
| _, _, true, _ , _ => true
| _, _, _, true, _ => true
| _, _, _, _, x => x
theorem f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Bool⊢ f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5)
b2:Boolb3:Boolb4:Boolb5:Bool⊢ true = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true → False⊢ true = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true → Falsex✝:b2 = true → False⊢ true = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true → Falsex✝¹:b2 = true → Falsex✝:b3 = true → False⊢ true = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true → Falsex✝²:b2 = true → Falsex✝¹:b3 = true → Falsex✝:b4 = true → False⊢ b5 = (b1 || b2 || b3 || b4 || b5)
all_goals All goals completed! 🐙
每种情况都包含一个排除先前情况的假设:
simp_all 策略会同时化简所有假设和目标,可以处理所有情况:
def f : Bool → Bool → Bool → Bool → Bool → Bool
| true, _, _, _ , _ => true
| _, true, _, _ , _ => true
| _, _, true, _ , _ => true
| _, _, _, true, _ => true
| _, _, _, _, x => x
theorem f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Bool⊢ f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5)
b2:Boolb3:Boolb4:Boolb5:Bool⊢ true = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true → False⊢ true = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true → Falsex✝:b2 = true → False⊢ true = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true → Falsex✝¹:b2 = true → Falsex✝:b3 = true → False⊢ true = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true → Falsex✝²:b2 = true → Falsex✝¹:b3 = true → Falsex✝:b4 = true → False⊢ b5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Bool⊢ true = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true → False⊢ true = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true → Falsex✝:b2 = true → False⊢ true = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true → Falsex✝¹:b2 = true → Falsex✝:b3 = true → False⊢ true = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true → Falsex✝²:b2 = true → Falsex✝¹:b3 = true → Falsex✝:b4 = true → False⊢ b5 = (b1 || b2 || b3 || b4 || b5) All goals completed! 🐙
8.7. 互递归
Lean 也支持互递归定义(mutual recursive definition)。语法类似于互归纳类型的语法。以下是一个例子:
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:Nat⊢ even (a + 1) = odd a
All goals completed! 🐙
example : odd (a + 1) = even a := a:Nat⊢ odd (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:Nat⊢ even 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 来定义。在底层,这被编译为单个递归定义。内部定义的函数将和类型(sum type)的一个元素作为参数,该元素要么是 even 的输入,要么是 odd 的输入。然后它返回适合该输入的输出。为了定义这个函数,Lean 使用了一个合适的良基度量。内部细节对用户是隐藏的;使用此类定义的规范方法是使用 simp 策略。
互递归定义也提供了处理互归纳和嵌套归纳类型的自然方式。回想一下之前介绍的 Even 和 Odd 作为互归纳谓词的定义。
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_zero、even_succ 和 odd_succ 提供了证明一个数是偶数或奇数的肯定方式。我们需要利用归纳类型是由这些构造子生成的事实,来知道零不是奇数,并且后两个蕴含关系是反向的。像往常一样,构造子保存在以被定义类型命名的命名空间中,命令 open Even Odd 使我们能够更方便地访问它们。
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
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
作为另一个例子,假设我们使用嵌套归纳类型来归纳地定义一组项,使得一个项要么是一个常量(由字符串给出名称),要么是将一个常量应用于常量列表的结果。
然后,我们可以使用互递归定义来计算项中出现的常量数量,以及项列表中出现的常量数量。
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"]
#eval numConsts sample
end Term
作为最后一个例子,我们定义一个函数 replaceConst a b e,它将项 e 中的常量 a 替换为 b,然后证明常量的数量相同。注意,我们的证明使用了互递归(也就是互归纳)。
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
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 Term⊢ numConstsLst (replaceConstLst a b es) = numConstsLst es
match es with
a:Stringb:Stringes:List Term⊢ numConstsLst (replaceConstLst a b []) = numConstsLst [] All goals completed! 🐙
a:Stringb:Stringes:List Termc:Termcs:List Term⊢ numConstsLst (replaceConstLst a b (c :: cs)) = numConstsLst (c :: cs)
All goals completed! 🐙
end
8.8. 依值模式匹配
我们在模式匹配一节中考虑的所有模式匹配的例子都可以很容易地用 casesOn 和 recOn 编写。然而,对于索引归纳族(indexed inductive family),如 Vect α n,情况往往不是这样,因为情况分割会对索引值施加约束。没有方程编译器,我们将需要大量样板代码来定义诸如 map、zip 和 unzip 这样非常简单的函数。要理解困难所在,考虑如何定义一个函数 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 这一点并不清楚。
一种解决方案是定义一个辅助函数:
set_option linter.unusedVariables false
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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
end Vect
在 nil 情况下,m 被实例化为 0,Nat.noConfusion 利用了 0 = n + 1 不可能发生。否则,v 的形式为 cons a as,我们可以简单地返回 as,在将其从长度为 m 的向量强制转换为长度为 n 的向量之后。
定义 tail 的困难在于维护索引之间的关系。m = n + 1 假设在 tailAux 中用于传递 n 和与次要前提关联的索引之间的关系。此外,0 = n + 1 的情况是不可达的,抛弃这种情况的规范方法是使用 Nat.noConfusion。
然而,tail 函数使用递归方程很容易定义,方程编译器自动为我们生成了所有的样板代码。以下是一些类似的例子:
set_option linter.unusedVariables false
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
end Vect
注意,我们可以省略"不可达"情况的递归方程,比如 head nil。为索引族自动生成的定义远非直接了当。例如:
set_option linter.unusedVariables false
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
#print zipWith#print zipWith.match_1end Vect
zipWith 函数手工定义起来比 tail 函数更加繁琐。我们鼓励你尝试使用 Vect.recOn、Vect.casesOn 和 Vect.noConfusion 来实现它。
8.9. 不可访问模式
有时,依值匹配模式中的一个参数对定义来说不是必需的,但必须包含进来以适当地特化表达式的类型。Lean 允许用户将这样的子项标记为模式匹配的不可访问(inaccessible)。当出现在左侧的项既不是变量也不是构造子应用时,这些注解是必不可少的,因为它们不适合作为模式匹配的目标。我们可以将这种不可访问模式视为模式中"不关心"的组件。你可以通过写 .(t) 来声明一个子项为不可访问。如果不可访问模式可以推断,你也可以写 _。
在下面的例子中,我们声明了一个归纳类型,定义了"在 f 的像中"这一属性。你可以将类型 ImageOf f b 的一个元素视为 b 在 f 的像中的证据,其中构造子 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
| n+1, cons a as, cons b bs => cons (a + b) (add as bs)
参数 {n : Nat} 出现在冒号之后,因为它不能在定义中保持不变。在实现这个定义时,方程编译器首先根据第一个参数是 0 还是形如 n+1 进行情况区分。然后对接下来两个参数进行嵌套的情况分割,在每种情况下,方程编译器会排除与第一个模式不兼容的情况。
但事实上,不需要对第一个参数进行情况分割;Vect 的 casesOn 消去子(eliminator)会自动抽象这个参数,并在我们对第二个参数进行情况分割时将其替换为 0 和 n + 1。使用不可访问模式,我们可以提示方程编译器避免对 n 进行情况分割。
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
end Vect
将位置标记为不可访问模式告诉方程编译器:第一,参数的形式应由其他参数施加的约束推断出来;第二,第一个参数不应参与模式匹配。
不可访问模式 .(_) 为了方便可以写成 _。
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
end Vect
正如我们上面提到的,参数 {n : Nat} 是模式匹配的一部分,因为它不能在定义中保持不变。Lean 不要求显式提供这些判别式,而是自动隐式地包含这些额外的判别式。
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
end Vect
当与自动绑定隐式参数(auto bound implicits)特性结合使用时,你可以进一步简化声明并写成:
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
def add [Add α] : Vect α n → Vect α n → Vect α n
| nil, nil => nil
| cons a as, cons b bs => cons (a + b) (add as bs)
end Vect
使用这些新特性,你可以更紧凑地编写前面几节定义的其他向量函数,如下所示:
set_option linter.unusedVariables false
inductive Vect (α : Type u) : Nat → Type u
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n+1)
namespace Vect
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)
end Vect
8.10. Match 表达式
Lean 还为许多函数式语言中的 match-with 表达式提供了一个编译器:
set_option linter.unusedVariables false
def isNotZero (m : Nat) : Bool :=
match m with
| 0 => false
| n + 1 => true
这看起来与普通的模式匹配定义没有太大区别,但关键在于 match 可以在表达式中的任何位置使用,并且可以带有任意参数。
set_option linter.unusedVariables false
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
#eval foo 7 true false
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. 练习
-
打开一个命名空间
Hidden以避免命名冲突,并使用方程编译器在自然数上定义加法、乘法和指数运算。然后使用方程编译器推导它们的一些基本性质。 -
类似地,使用方程编译器在列表上定义一些基本操作(如
reverse函数),并通过归纳证明关于列表的定理(例如,对于任意列表xs,reverse (reverse xs) = xs成立)。 -
在自然数上定义你自己的值域递归函数。类似地,看看你是否能自己定义
WellFounded.fix。 -
参照依值模式匹配一节中的例子,定义一个 append 两个向量的函数。这有些棘手;你需要定义一个辅助函数。
-
考虑以下算术表达式的类型。其思想是
varn是一个变量vₙ,而constn是一个常量,其值为n。inductive Expr where | const : Nat → Expr | var : Nat → Expr | plus : Expr → Expr → Expr | times : Expr → Expr → Expr deriving Repropen Exprdef sampleExpr : Expr := plus (times (var 0) (const 7)) (times (const 2) (var 1))这里
sampleExpr表示(v₀ * 7) + (2 * v₁)。编写一个求值此类表达式的函数,将每个
var n求值为v 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))def eval (v : Nat → Nat) : Expr → Nat | const n => sorry | var n => v n | plus e₁ e₂ => sorry | times e₁ e₂ => sorrydef sampleVal : Nat → Nat | 0 => 5 | 1 => 6 | _ => 0-- Try it out. You should get 47 here. -- #eval eval sampleVal sampleExpr实现"常量折叠(constant fusion)",一种将
5 + 7这样的子项简化为12的过程。使用辅助函数simpConst,定义一个"fuse"函数:要简化一个加法或乘法,首先递归地简化参数,然后应用simpConst来尝试简化结果。inductive Expr where | const : Nat → Expr | var : Nat → Expr | plus : Expr → Expr → Expr | times : Expr → Expr → Expr deriving Repr open Expr def eval (v : Nat → Nat) : Expr → Nat | const n => sorry | var n => v n | plus e₁ e₂ => sorry | times e₁ e₂ => sorrydef simpConst : Expr → Expr | plus (const n₁) (const n₂) => const (n₁ + n₂) | times (const n₁) (const n₂) => const (n₁ * n₂) | e => edef fuse : Expr → Expr := sorrytheorem simpConst_eq (v : Nat → Nat) : ∀ e : Expr, eval v (simpConst e) = eval v e := sorrytheorem fuse_eq (v : Nat → Nat) : ∀ e : Expr, eval v (fuse e) = eval v e := sorry最后两个定理表明这些定义保持了值不变。