8. 归纳和递归
在上一章中,我们看到归纳定义提供了在 Lean 中引入新类型的强大手段。此外, 构造子和递归器提供了在这些类型上定义函数的唯一手段。根据 命题即类型 对应关系, 这意味着归纳法是证明的基本方法。
Lean 提供了定义递归函数、执行模式匹配和编写归纳证明的自然方法。它允许你通过指定它应该满足的方程来定义一个函数, 它允许你通过指定如何处理可能出现的各种情况来证明一个定理。在它内部,这些描述被“编译”成原始递归器, 使用一个我们称之为“方程编译器”的程序。方程编译器不是可信代码库的一部分;它的输出包括由内核独立检查的项。
8.1. 模式匹配
对示意图模式的解释是编译过程的第一步。我们已经看到,casesOn 递归器可以通过分情况讨论来定义函数和证明定理,
根据归纳定义类型所涉及的构造子。但是复杂的定义可能会使用几个嵌套的 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
用来定义这些函数的方程在定义上是成立的:
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
我们可以用一些更耳熟能详的符号,而不是 zero 和 succ:
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 简单地将这些表达式规范化,
直到显示构造子 zero 和 succ。
模式匹配适用于任何归纳类型,如乘积和 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
在这里,我们不仅用它来定义一个函数,而且还用它来进行逐情况证明:
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
模式匹配也可以用来解构归纳定义的命题:
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
这样解决带逻辑连接词的命题就很紧凑。
在所有这些例子中,模式匹配被用来进行单一情况的区分。更有趣的是,模式可以涉及嵌套的构造子,如下面的例子。
方程编译器首先对输入是 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
还要注意的是,当定义中不需要一个参数的值时,你可以用下划线来代替。这个下划线被称为 通配符模式 ,或 匿名变量 。与方程编译器之外的用法不同,这里的下划线 并不 表示一个隐参数。使用下划线表示通配符在函数式编程语言中是很常见的,所以 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 也可以处理更复杂的模式匹配形式,其中从属类型的参数对各种情况构成了额外的约束。 这种 依值模式匹配 的例子在 依值模式匹配一节中考虑。
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 的定义是否满足与之前相同的定义特性。
一些函数式编程语言支持 不完整的模式 。在这些语言中,解释器对不完整的情况产生一个异常或返回一个任意的值。
我们可以使用 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。
8.3. 结构化递归和归纳
方程编译器的强大之处在于,它还支持递归定义。在接下来的三节中,我们将分别介绍。
-
结构性递归定义
-
良基的递归定义
-
相互递归的定义
一般来说,方程编译器处理以下形式的输入:
def foo (a : α) : (b : β) → γ | [patterns₁] => t₁ ... | [patternsₙ] => tₙ
这里 (a : α) 是一个参数序列,(b : β) 是进行模式匹配的参数序列,γ 是任何类型,
它可以取决于 a 和 b。每一行应该包含相同数量的模式,对应于 β 的每个元素。
正如我们所看到的,模式要么是一个变量,要么是应用于其他模式的构造子,要么是一个正规化为该形式的表达式
(其中非构造子用 [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
| 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: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 : 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:
#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.loop
Lean 为每个 let rec 创建一个辅助声明。在上面的例子中,它为出现在 replicate 中的 let rec loop 创建了声明 replicate.loop。
注意,Lean 通过将出现在 let rec 声明中的任何局部变量添加为附加参数来“闭合”声明。例如,局部变量 a 出现在 let rec loop 中。
你也可以在策略模式下使用 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. 良基递归和归纳
当无法使用结构递归时,我们可以使用良基递归来证明终止。 我们需要一个良基关系,以及一个证明,证明每个递归应用相对于该关系都是递减的。 依赖类型理论足够强大,可以编码和证明良基递归。让我们从理解其工作原理所需的逻辑背景开始。
Lean 的标准库定义了两个谓词 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,以及假设 h,即 r 是良基的。
变量 C 代表递归定义的动机:对于每个元素 x : α,我们想要构造一个 C x 的元素。
函数 F 提供了这样做的归纳配方:它告诉我们如何构造一个元素 C x,给定 x 的每个前驱 y 的 C 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
#reduce div 8 2繁饰器(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 应用。
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 函数可以直接定义,因为它可以被自然数上字典序的良基性证明。
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 子句是可选的:
注意,在上面的例子中使用了字典序,因为实例 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 等同于使用一个新的公理,应该避免。在下面的例子中,我们使用 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 应用于 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 对应于递归调用,其中 ih1 匹配嵌套递归调用。
再一次,归纳假设是合适的。
使用 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
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 递归定义的。
在底层,这被编译为单个递归定义。内部定义的函数接受一个和类型的元素作为参数,该元素要么是 even 的输入,要么是 odd 的输入。
然后它返回适合输入的输出。为了定义该函数,Lean 使用适当的良基度量。
内部细节旨在对用户隐藏;使用此类定义的规范方法是使用 simp(或 unfold),正如我们在上面所做的那样。
相互递归定义还提供了处理相互和嵌套归纳类型的自然方法。回想一下之前介绍的作为相互归纳谓词的 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 编写。
然而,对于像 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 的困难在于维护索引之间的关系。tailAux 中的假设 m = n + 1 用于传达 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 允许用户将此类子项标记为 不可访问 以进行模式匹配。
例如,当出现在左侧的项既不是变量也不是构造函数应用时,这些注释是必不可少的,因为这些不是模式匹配的合适目标。
我们可以将此类不可访问模式视为模式的“不关心”组件。
你可以通过编写 .(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 消除器会自动抽象此参数并将其替换为 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
当与 自动绑定隐式 功能结合使用时,你可以进一步简化声明并编写:
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. 匹配表达式
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以避免命名冲突,并使用方程编译器定义自然数上的加法、乘法和幂运算。 然后使用方程编译器推导它们的一些基本属性。
-
定义你自己的函数来对自然数执行值域递归。同样,看看你是否能弄清楚如何自己定义
WellFounded.fix。
-
按照 依赖模式匹配 一节中的示例,定义一个将两个向量连接起来的函数。 这很棘手;你必须定义一个辅助函数。
-
考虑以下算术表达式类型。其思想是
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))-- Here
sampleExprrepresents(v₀ * 7) + (2 * v₁).这里
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-- 试一试。你应该在这里得到 47。 -- #eval eval sampleVal sampleExpr实现“常量融合”,这是一个将子项(如
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最后两个定理表明这些定义保留了值。