7. 归纳类型
我们已经看到 Lean 的形式基础包括基本类型,Prop、Type 0、Type 1、Type 2 等,
并允许形成依赖函数类型 (x : α) → β。在例子中,我们还使用了额外的类型,如 Bool、Nat 和 Int,
以及类型构造子,如 List 和乘积 ×。事实上,在 Lean 的库中,除了宇宙之外的每一个具体类型和除了依赖箭头之外的每一个类型构造子都是一个被称为 归纳类型 的类型构造的一般类别的实例。
值得注意的是,仅用类型宇宙、依赖箭头类型和归纳类型就可以构建一个内涵丰富的数学大厦;其他一切都源于这些。
直观地说,一个归纳类型是由一个指定的构造子列表建立起来的。在 Lean 中,指定这种类型的语法如下:
inductive Foo where | constructor₁ : ... → Foo | constructor₂ : ... → Foo ... | constructorₙ : ... → Foo
我们的直觉是,每个构造子都指定了一种建立新的对象 Foo 的方法,可能是由以前构造的值构成。
Foo 类型只不过是由以这种方式构建的对象组成。
我们将在下面看到,构造子的参数可以包括 Foo 类型的对象,但要遵守一定的“正向性”约束,
即保证 Foo 的元素是自下而上构建的。粗略地说,每个 ... 可以是由 Foo 和以前定义的类型构建的任何箭头类型,
其中 Foo 如果出现,也只是作为依赖箭头类型的“目标”。
我们将提供一些归纳类型的例子。我们还把上述方案稍微扩展,即相互定义的归纳类型,以及所谓的 归纳族。
就像逻辑连接词一样,每个归纳类型都有引入规则,说明如何构造该类型的一个元素;还有消去规则,说明如何在另一个构造中“使用”该类型的一个元素。 其实逻辑连接词也是归纳类型结构的例子。你已经看到了归纳类型的引入规则:它们只是在类型的定义中指定的构造子。 消去规则提供了类型上的递归原则,其中也包括作为一种特殊情况的归纳原则。
在下一章中,我们将介绍 Lean 的函数定义包,它提供了更方便的方法来定义归纳类型上的函数并进行归纳证明。 但是由于归纳类型的概念是如此的基本,我们觉得从低级的、实践的理解开始是很重要的。 我们将从归纳类型的一些基本例子开始,然后逐步上升到更详细和复杂的例子。
7.1. 枚举类型
最简单的归纳类型是一个具有有限的、可枚举的元素列表的类型。
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
inductive 命令创建了一个新类型 Weekday。构造子都在 Weekday 命名空间中。
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
#check Weekday.sunday
#check Weekday.monday
open Weekday
#check sunday
#check monday
在声明 Weekday 归纳类型时,可以省略 : Weekday。
inductive Weekday where
| sunday
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
把 sunday、monday、... 、saturday 看作是 Weekday 的不同元素,没有其他有区别的属性。
消去规则 Weekday.rec,与 Weekday 类型及其构造子一起定义。它也被称为 递归器(Recursor),
它是使该类型“归纳”的原因:它允许我们通过给每个构造子分配相应的值来定义 Weekday 的函数。
直观的说,归纳类型是由构造子详尽地生成的,除了它们构造的元素外,没有其他元素。
Weekday.rec.{u} {motive : Weekday → Sort u}
(sunday : motive Weekday.sunday)
(monday : motive Weekday.monday)
(tuesday : motive Weekday.tuesday)
(wednesday : motive Weekday.wednesday)
(thursday : motive Weekday.thursday)
(friday : motive Weekday.friday)
(saturday : motive Weekday.saturday)
(t : Weekday) :
motive t
我们将使用 match 表达式来定义一个从 Weekday 到自然数的函数:
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
open Weekday
def numberOfDay (d : Weekday) : Nat :=
match d with
| sunday => 1
| monday => 2
| tuesday => 3
| wednesday => 4
| thursday => 5
| friday => 6
| saturday => 7
#eval numberOfDay Weekday.sunday
#eval numberOfDay Weekday.monday
#eval numberOfDay Weekday.tuesday
在使用 Lean 的逻辑时,match 表达式是使用声明归纳类型时生成的 递归器(recursor) Weekday.rec 来编译的。
这确保了结果项在类型论中是良定义的。对于编译后的代码,match 的编译方式与其他函数式编程语言相同。
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
open Weekday
def numberOfDay (d : Weekday) : Nat :=
match d with
| sunday => 1
| monday => 2
| tuesday => 3
| wednesday => 4
| thursday => 5
| friday => 6
| saturday => 7
set_option pp.all true
#print numberOfDay
#print numberOfDay.match_1
#print Weekday.casesOn
#check @Weekday.rec
当声明一个归纳数据类型时,你可以使用 deriving Repr 来指示 Lean 生成一个函数,将 Weekday 对象转换为文本。
这个函数被 #eval 命令用来显示 Weekday 对象。如果不存在 Repr,#eval 会尝试当场派生一个。
inductive Weekday where
| sunday
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
deriving Repr
open Weekday
#eval tuesday
将与某一结构相关的定义和定理归入同名的命名空间通常很有用。例如,我们可以将 numberOfDay 函数放在 Weekday 命名空间中。
然后当我们打开命名空间时,我们就可以使用较短的名称。
我们可以定义从 Weekday 到 Weekday 的函数:
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
match d with
| sunday => monday
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => saturday
| saturday => sunday
def previous (d : Weekday) : Weekday :=
match d with
| sunday => saturday
| monday => sunday
| tuesday => monday
| wednesday => tuesday
| thursday => wednesday
| friday => thursday
| saturday => friday
#eval next (next tuesday)
#eval next (previous tuesday)
example : next (previous tuesday) = tuesday :=
rfl
end Weekday
我们如何证明对于任何 Weekday d,next (previous d) = d 的一般定理?
你可以使用 match 为每个构造子提供该主张的证明:
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
match d with
| sunday => monday
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => saturday
| saturday => sunday
def previous (d : Weekday) : Weekday :=
match d with
| sunday => saturday
| monday => sunday
| tuesday => monday
| wednesday => tuesday
| thursday => wednesday
| friday => thursday
| saturday => friday
theorem next_previous (d : Weekday) : next (previous d) = d :=
match d with
| sunday => rfl
| monday => rfl
| tuesday => rfl
| wednesday => rfl
| thursday => rfl
| friday => rfl
| saturday => rfl
使用策略证明,我们可以更简洁:
inductive Weekday where
| sunday : Weekday
| monday : Weekday
| tuesday : Weekday
| wednesday : Weekday
| thursday : Weekday
| friday : Weekday
| saturday : Weekday
deriving Repr
namespace Weekday
def next (d : Weekday) : Weekday :=
match d with
| sunday => monday
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => saturday
| saturday => sunday
def previous (d : Weekday) : Weekday :=
match d with
| sunday => saturday
| monday => sunday
| tuesday => monday
| wednesday => tuesday
| thursday => wednesday
| friday => thursday
| saturday => friday
theorem next_previous (d : Weekday) : next (previous d) = d := d:Weekday⊢ d.previous.next = d
⊢ sunday.previous.next = sunday⊢ monday.previous.next = monday⊢ tuesday.previous.next = tuesday⊢ wednesday.previous.next = wednesday⊢ thursday.previous.next = thursday⊢ friday.previous.next = friday⊢ saturday.previous.next = saturday ⊢ sunday.previous.next = sunday⊢ monday.previous.next = monday⊢ tuesday.previous.next = tuesday⊢ wednesday.previous.next = wednesday⊢ thursday.previous.next = thursday⊢ friday.previous.next = friday⊢ saturday.previous.next = saturday All goals completed! 🐙
下面的 归纳类型的策略 将介绍专门设计用于利用归纳类型的其他策略。
注意,在 命题即类型 的对应关系下,我们可以使用 match 来证明定理以及定义函数。
换句话说,在 命题即类型 的对应关系下,按情况证明是一种按情况定义,其中被“定义”的是证明而不是数据。
Lean 库中的 Bool 类型是枚举类型的一个实例。
(为了运行这些例子,我们将它们放在一个名为 Hidden 的命名空间中,这样像 Bool 这样的名字就不会与标准库中的 Bool 冲突。
这是必要的,因为这些类型是 Lean “prelude”的一部分,在系统启动时会自动导入。)
作为一个练习,你应该思考这些类型的引入和消去规则是做什么的。作为进一步的练习,我们建议在 Bool 类型上定义布尔运算 and、or、not,
并验证常见的恒等式。注意,你可以使用 match 定义像 and 这样的二元运算:
namespace Hidden
def and (a b : Bool) : Bool :=
match a with
| true => b
| false => false
end Hidden
同样,大多数恒等式可以通过引入合适的 match,然后使用 rfl 来证明。
7.2. 带参数的构造子
枚举类型是归纳类型的一个非常特殊的情况,其中的构造子完全不接受参数。一般来说,一个“构造”可以依赖于数据, 然后这些数据在构造的参数中表示出来。考虑一下库中乘积类型和和类型的定义:
namespace Hidden
inductive Prod (α : Type u) (β : Type v)
| mk : α → β → Prod α β
inductive Sum (α : Type u) (β : Type v) where
| inl : α → Sum α β
| inr : β → Sum α β
end Hidden
考虑一下这些例子中发生了什么。乘积类型有一个构造子 Prod.mk,它接受两个参数。
为了定义 Prod α β 上的函数,我们可以假设输入的形式是 Prod.mk a b,并且我们必须根据 a 和 b 指定输出。
我们可以用这个来定义 Prod 的两个投影。请记住,标准库为 Prod α β 定义了符号 α × β,
为 Prod.mk a b 定义了符号 (a, b)。
namespace Hidden
inductive Prod (α : Type u) (β : Type v)
| mk : α → β → Prod α β
def fst {α : Type u} {β : Type v} (p : Prod α β) : α :=
match p with
| Prod.mk a b => a
def snd {α : Type u} {β : Type v} (p : Prod α β) : β :=
match p with
| Prod.mk a b => b
end Hidden
函数 fst 接受一个对子 p。match 将 p 解释为一个对子 Prod.mk a b。
还记得在 依赖类型理论 中,为了使这些定义尽可能通用,我们允许类型 α 和 β 属于任何宇宙。
这是另一个例子,我们使用递归器 Prod.casesOn 而不是 match。
def prod_example (p : Bool × Nat) : Nat :=
Prod.casesOn (motive := fun _ => Nat) p
(fun b n => cond b (2 * n) (2 * n + 1))
#eval prod_example (true, 3)
#eval prod_example (false, 3)
参数 motive 用于指定你想要构造的对象的类型,它是一个函数,因为它可能依赖于该对子。
cond 函数是一个布尔条件:如果 b 为真,cond b t1 t2 返回 t1,否则返回 t2。
函数 prod_example 接受一个由布尔值 b 和数字 n 组成的对子,
并根据 b 是真还是假返回 2 * n 或 2 * n + 1。
相比之下,和类型有 两个 构造子,inl 和 inr(代表“左插入”和“右插入”),每个构造子接受 一个(显式)参数。
为了定义 Sum α β 上的函数,我们必须处理两种情况:要么输入的形式是 inl a,在这种情况下我们必须根据 a 指定输出值;
要么输入的形式是 inr b,在这种情况下我们必须根据 b 指定输出值。
def sum_example (s : Sum Nat Nat) : Nat :=
Sum.casesOn (motive := fun _ => Nat) s
(fun n => 2 * n)
(fun n => 2 * n + 1)
#eval sum_example (Sum.inl 3)
#eval sum_example (Sum.inr 3)
这个例子与前一个类似,但现在 sum_example 的输入隐式地要么是 inl n 的形式,要么是 inr n 的形式。
在第一种情况下,函数返回 2 * n,在第二种情况下,它返回 2 * n + 1。
注意,乘积类型依赖于参数 α β : Type,它们既是构造子的参数,也是 Prod 的参数。
当这些参数可以从构造子的后续参数或返回类型中推断出来时,Lean 会检测到并将它们设为隐式。
在 定义自然数 中,我们将看到当归纳类型的构造子接受来自归纳类型本身的参数时会发生什么。 我们在本节中考虑的例子的特点是,每个构造子只依赖于先前指定的类型。
注意,具有多个构造子的类型是析取的:Sum α β 的元素要么是 inl a 的形式,要么 是 inr b 的形式。
具有多个参数的构造子引入了合取信息:从 Prod α β 的元素 Prod.mk a b 中,我们可以提取 a 和 b。
任意归纳类型可以包含这两个特征,即拥有任意数量的构造子,每个构造子接受任意数量的参数。
与函数定义一样,Lean 的归纳定义语法允许你将构造子的命名参数放在冒号之前:
namespace Hidden
inductive Prod (α : Type u) (β : Type v) where
| mk (fst : α) (snd : β) : Prod α β
inductive Sum (α : Type u) (β : Type v) where
| inl (a : α) : Sum α β
| inr (b : β) : Sum α β
end Hidden
这些定义的结果本质上与本节前面给出的结果相同。
像 Prod 这样只有一个构造子的类型是纯合取的:构造子只是将参数列表打包成单个数据,本质上是一个元组,
其中后续参数的类型可以依赖于初始参数的类型。我们也可以把这种类型看作是“记录”或“结构”。
在 Lean 中,关键字 structure 可以用来同时定义这种归纳类型及其投影。
这个例子同时引入了归纳类型 Prod、它的构造子 mk、通常的消去器(rec 和 recOn),
以及如上定义的投影 fst 和 snd。
如果你不命名构造子,Lean 默认使用 mk。例如,下面定义了一个记录,将颜色存储为 RGB 值的元组:
structure Color where
red : Nat
green : Nat
blue : Nat
deriving Repr
def yellow := Color.mk 255 255 0
#eval Color.red yellow
yellow 的定义用显示的三个值形成了记录,投影 Color.red 返回红色分量。
structure 命令对于定义代数结构特别有用,Lean 提供了大量的基础设施来支持它们的使用。例如,这里是半群的定义:
structure Semigroup where
carrier : Type u
mul : carrier → carrier → carrier
mul_assoc : ∀ a b c, mul (mul a b) c = mul a (mul b c)
我们将在 结构体与记录 一章中看到更多例子。
我们已经讨论了依赖乘积类型 Sigma:
namespace Hidden
inductive Sigma {α : Type u} (β : α → Type v) where
| mk : (a : α) → β a → Sigma β
end Hidden
库中还有两个归纳类型的例子:
namespace Hidden
inductive Option (α : Type u) where
| none : Option α
| some : α → Option α
inductive Inhabited (α : Type u) where
| mk : α → Inhabited α
end Hidden
在依赖类型理论的语义中,没有内置的部分函数的概念。函数类型 α → β 或依赖函数类型 (a : α) → β 的每个元素都被假定在每个输入上都有值。
Option 类型提供了一种表示部分函数的方法。Option β 的元素要么是 none,要么是 some b 的形式,其中 b : β。
因此,我们可以把类型 α → Option β 的元素 f 看作是从 α 到 β 的部分函数:
对于每个 a : α,f a 要么返回 none(表示 f a “未定义”),要么返回 some b。
Inhabited α 的元素只是 α 中存在元素这一事实的见证。稍后,我们将看到 Inhabited 是 Lean 中 类型类 的一个例子:
可以指示 Lean 适当的基本类型是居留的,并在此基础上自动推断其他构造类型也是居留的。
作为练习,我们鼓励你开发从 α 到 β 以及从 β 到 γ 的部分函数的组合概念,并证明它的行为符合预期。
我们还鼓励你证明 Bool 和 Nat 是居留的,两个居留类型的乘积是居留的,以及到居留类型的函数的类型是居留的。
7.3. 归纳定义的命题
归纳定义的类型可以存在于任何类型宇宙中,包括最底层的 Prop。事实上,这正是逻辑连接词的定义方式。
namespace Hidden
inductive False : Prop
inductive True : Prop where
| intro : True
inductive And (a b : Prop) : Prop where
| intro : a → b → And a b
inductive Or (a b : Prop) : Prop where
| inl : a → Or a b
| inr : b → Or a b
end Hidden
你应该思考这些是如何产生你已经看到的引入和消去规则的。有一些规则管理归纳类型的消去器可以消去 到 什么,
也就是说,什么样的类型可以是递归器的目标。粗略地说,Prop 中归纳类型的特征是,人们只能消去到 Prop 中的其他类型。
这与如果 p : Prop,则元素 hp : p 不携带数据的理解是一致的。
然而,这个规则有一个小的例外,我们将在下面的 归纳族 中讨论。
甚至存在量词也是归纳定义的:
namespace Hidden
inductive Exists {α : Sort u} (p : α → Prop) : Prop where
| intro (w : α) (h : p w) : Exists p
end Hidden
请记住,符号 ∃ x : α, p 是 Exists (fun x : α => p) 的语法糖。
False、True、And 和 Or 的定义与 Empty、Unit、
Prod 和 Sum 的定义完全类似。区别在于第一组产生 Prop 的元素,
而第二组产生某个 u 的 Type u 的元素。类似地,∃ x : α, p 是 Σ x : α, β 的 Prop 值变体。
这里适合提及另一种归纳类型,记作 {x : α // p},它是 ∃ x : α, p 和 Σ x : α, β 的一种混合体。
namespace Hidden
inductive Subtype {α : Type u} (p : α → Prop) where
| mk : (x : α) → p x → Subtype p
end Hidden
事实上,在 Lean 中,Subtype 是使用 structure 命令定义的:
namespace Hidden
structure Subtype {α : Sort u} (p : α → Prop) where
val : α
property : p val
end Hidden
符号 {x : α // p x} 是 Subtype (fun x : α => p x) 的语法糖。
它模仿了集合论中的子集符号:其思想是 {x : α // p x} 表示 α 中具有属性 p 的元素集合。
7.4. 定义自然数
到目前为止,我们所看到的归纳定义的类型都是“平坦的”:构造子打包数据并将其插入到一个类型中,而相应的递归器则解压数据并对其进行操作。
当构造子作用于被定义的类型中的元素时,事情就变得更加有趣了。一个典型的例子是自然数 Nat 类型:
有两个构造子,我们从 zero : Nat 开始;它不需要参数,所以我们一开始就有了它。相反,构造子 succ 只能应用于先前构造的 Nat。
将其应用于 zero,得到 succ zero : Nat。再次应用它可以得到 succ (succ zero) : Nat,依此类推。
直观地说,Nat 是具有这些构造子的“最小”类型,这意味着它是通过从 zero 开始并重复应用 succ 这样无穷无尽地(并且自由地)生成的。
和以前一样,Nat 的递归器被设计用来定义一个从 Nat 到任何域的依赖函数 f,也就是一个 (n : Nat) → motive n 的元素 f,
其中 motive : Nat → Sort u。它必须处理两种情况:一种是输入为 zero 的情况,另一种是输入为 succ n 的情况,其中 n : Nat。
在第一种情况下,我们只需指定一个适当类型的目标值,就像以前一样。但是在第二种情况下,递归器可以假设在 n 处的 f 的值已经被计算过了。
因此,递归器的下一个参数是以 n 和 f n 来指定 f (succ n) 的值。如果检查递归器的类型,你会得到:
Nat.rec.{u} :
{motive : Nat → Sort u} →
(zero : motive Nat.zero) →
(succ : (n : Nat) → motive n → motive (Nat.succ n)) →
(t : Nat) → motive t
隐参数 motive,是被定义的函数的陪域。在类型论中,通常说 motive 是消去/递归的 目的 ,因为它描述了我们希望构建的对象类型。
接下来的两个参数指定了如何计算零和后继的情况,如上所述。它们也被称为 小前提。
最后,t : Nat 是函数的输入。它也被称为 大前提。
Nat.recOn 与 Nat.rec 类似,但大前提发生在小前提之前。
Nat.recOn.{u} :
{motive : Nat → Sort u} →
(t : Nat) →
(zero : motive Nat.zero) →
(succ : ((n : Nat) → motive n → motive (Nat.succ n))) →
motive t
例如,考虑自然数上的加法函数 add m n。固定 m,我们可以通过递归来定义 n 的加法。在基本情况下,我们将 add m zero 设为 m。
在后续步骤中,假设 add m n 的值已经确定,我们将 add m (succ n) 定义为 succ (add m n)。
namespace Hidden
inductive Nat where
| zero : Nat
| succ : Nat → Nat
deriving Repr
def add (m n : Nat) : Nat :=
match n with
| Nat.zero => m
| Nat.succ n => Nat.succ (add m n)
open Nat
#eval add (succ (succ zero)) (succ zero)end Hidden
将这些定义放入一个命名空间 Nat 是很有用的。然后我们可以继续在这个命名空间中定义熟悉的符号。现在加法的两个定义方程是定义上成立的:
namespace Nat
def add (m n : Nat) : Nat :=
match n with
| Nat.zero => m
| Nat.succ n => Nat.succ (add m n)
instance : Add Nat where
add := add
theorem add_zero (m : Nat) : m + zero = m := rfl
theorem add_succ (m n : Nat) : m + succ n = succ (m + n) := rfl
end Nat
end Hidden
我们将在 类型类 一章中解释 instance 命令如何工作。在下面的例子中,我们将使用 Lean 自己的自然数版本。
然而,证明像 0 + n = n 这样的事实,需要用归纳法证明。如上所述,归纳原则只是递归原则的一个特例,其中陪域 motive n 是 Prop 的一个元素。
它代表了熟悉的归纳证明模式:要证明 ∀ n, motive n,首先要证明 motive 0,然后对于任意的 n,假设 ih : motive n 并证明 motive (n + 1)。
namespace Hidden
open Nat
theorem zero_add (n : Nat) : 0 + n = n :=
Nat.recOn (motive := fun x => 0 + x = x)
n
(show 0 + 0 = 0 from rfl)
(fun (n : Nat) (ih : 0 + n = n) =>
show 0 + (n + 1) = n + 1 from
calc 0 + (n + 1)
_ = (0 + n) + 1 := rfl
_ = n + 1 := n✝:Natn:Natih:0 + n = n⊢ 0 + n + 1 = n + 1 All goals completed! 🐙)
end Hidden
请注意,当 Nat.recOn 在证明中使用时,它实际上是变相的归纳原则。rw 和 simp 策略在这样的证明中往往非常有效。
在这种情况下,证明可以化简成:
namespace Hidden
open Nat
theorem zero_add (n : Nat) : 0 + n = n :=
Nat.recOn (motive := fun x => 0 + x = x) n
rfl
(fun n ih => n✝:Natn:Natih:(fun x => 0 + x = x) n⊢ (fun x => 0 + x = x) n.succ All goals completed! 🐙)
end Hidden
另一个例子,让我们证明加法结合律,∀ m n k, m + n + k = m + (n + k)。
(我们定义的符号 + 是向左结合的,所以 m + n + k 实际上是 (m + n) + k。)
最难的部分是确定在哪个变量上做归纳。由于加法是由第二个参数的递归定义的,k 是一个很好的猜测,一旦我们做出这个选择,证明几乎是顺理成章的:
namespace Hidden
open Nat
theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) :=
Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k
(show m + n + 0 = m + (n + 0) from rfl)
(fun k (ih : m + n + k = m + (n + k)) =>
show m + n + (k + 1) = m + (n + (k + 1)) from
calc m + n + (k + 1)
_ = (m + n + k) + 1 := rfl
_ = (m + (n + k)) + 1 := m:Natn:Natk✝:Natk:Natih:m + n + k = m + (n + k)⊢ m + n + k + 1 = m + (n + k) + 1 All goals completed! 🐙
_ = m + ((n + k) + 1) := rfl
_ = m + (n + (k + 1)) := rfl)
end Hidden
你又可以化简证明:
open Nat
theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) :=
Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k
rfl
(fun k ih => m:Natn:Natk✝:Natk:Natih:(fun k => m + n + k = m + (n + k)) k⊢ (fun k => m + n + k = m + (n + k)) k.succ m:Natn:Natk✝:Natk:Natih:(fun k => m + n + k = m + (n + k)) k⊢ m + (n + k) + 1 = m + (n + (k + 1)); All goals completed! 🐙)
假设我们试图证明加法交换律。选择第二个参数做归纳法,我们可以这样开始:
open Nat
theorem add_comm (m n : Nat) : m + n = n + m :=
Nat.recOn (motive := fun x => m + x = x + m) n
(show m + 0 = 0 + m All goals completed! 🐙 All goals completed! 🐙)
(fun (n : Nat) (ih : m + n = n + m) =>
show m + succ n = succ n + m from
calc m + succ n
_ = succ (m + n) := rfl
_ = succ (n + m) := m:Natn✝:Natn:Natih:m + n = n + m⊢ (m + n).succ = (n + m).succ All goals completed! 🐙
_ = succ n + m := sorry)
在这一点上,我们看到我们需要另一个事实,即 succ (n + m) = succ n + m。
你可以通过对 m 的归纳来证明这一点:
open Nat
theorem succ_add (n m : Nat) : succ n + m = succ (n + m) :=
Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m
(show succ n + 0 = succ (n + 0) from rfl)
(fun (m : Nat) (ih : succ n + m = succ (n + m)) =>
show succ n + succ m = succ (n + succ m) from
calc succ n + succ m
_ = succ (succ n + m) := rfl
_ = succ (succ (n + m)) := n:Natm✝:Natm:Natih:n.succ + m = (n + m).succ⊢ (n.succ + m).succ = (n + m).succ.succ All goals completed! 🐙
_ = succ (n + succ m) := rfl)
然后你可以用 succ_add 代替前面证明中的 sorry。然而,证明可以再次压缩:
namespace Hidden
inductive Nat where
| zero : Nat
| succ : Nat → Nat
deriving Repr
def add (m n : Nat) : Nat :=
match n with
| Nat.zero => m
| Nat.succ n => Nat.succ (add m n)
instance : Add Nat where
add := add
namespace Nat
theorem add_zero (m : Nat) : m + zero = m := rfl
theorem add_succ (m n : Nat) : m + succ n = succ (m + n) := rfl
theorem zero_add (n : Nat) : zero + n = n :=
Nat.recOn (motive := fun x => zero + x = x) n
rfl
(fun n ih => n✝:Natn:Natih:(fun x => zero + x = x) n⊢ (fun x => zero + x = x) n.succ All goals completed! 🐙)
end Nat
open Nat
theorem succ_add (n m : Nat) : succ n + m = succ (n + m) :=
Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m
rfl
(fun m ih => n:Natm✝:Natm:Natih:(fun x => n.succ + x = (n + x).succ) m⊢ (fun x => n.succ + x = (n + x).succ) m.succ All goals completed! 🐙)
theorem add_comm (m n : Nat) : m + n = n + m :=
Nat.recOn (motive := fun x => m + x = x + m) n
(m:Natn:Nat⊢ (fun x => m + x = x + m) zero All goals completed! 🐙)
(fun m ih => m✝:Natn:Natm:Natih:(fun x => m✝ + x = x + m✝) m⊢ (fun x => m✝ + x = x + m✝) m.succ All goals completed! 🐙)
end Hidden
7.5. 其他递归数据类型
让我们考虑更多归纳定义类型的例子。对于任何类型 α,库中定义了 α 元素的列表类型 List α。
namespace Hidden
inductive List (α : Type u) where
| nil : List α
| cons (h : α) (t : List α) : List α
namespace List
def append (as bs : List α) : List α :=
match as with
| nil => bs
| cons a as => cons a (append as bs)
theorem nil_append (as : List α) : append nil as = as :=
rfl
theorem cons_append (a : α) (as bs : List α) :
append (cons a as) bs = cons a (append as bs) :=
rfl
end List
end Hidden
α 类型的元素列表,要么是空列表 nil,要么是一个元素 h : α,后面是一个列表 t : List α。
第一个元素 h,通常被称为列表的“头”,剩余部分 t,被称为“尾”。
作为一个练习,请证明以下内容:
namespace Hidden
inductive List (α : Type u) where
| nil : List α
| cons : α → List α → List α
namespace List
def append (as bs : List α) : List α :=
match as with
| nil => bs
| cons a as => cons a (append as bs)
theorem nil_append (as : List α) : append nil as = as :=
rfl
theorem cons_append (a : α) (as bs : List α)
: append (cons a as) bs = cons a (append as bs) :=
rfl
theorem append_nil (as : List α) :
append as nil = as :=
sorry
theorem append_assoc (as bs cs : List α) :
append (append as bs) cs = append as (append bs cs) :=
sorry
end List
end Hidden
还可以尝试定义函数 length : {α : Type u} → List α → Nat,返回一个列表的长度,并证明它的行为符合预期(例如,length (append as bs) = length as + length bs)。
另一个例子,我们可以定义二叉树的类型:
inductive BinaryTree where
| leaf : BinaryTree
| node : BinaryTree → BinaryTree → BinaryTree
事实上,我们甚至可以定义可数多叉树的类型:
7.6. 归纳类型的策略
归纳类型在 Lean 中有最根本的重要性,因此设计了一些方便使用的策略,这里讲几种。
cases 策略适用于归纳定义类型的元素,正如其名称所示:它根据每个可能的构造子分解元素。
在其最基本的形式中,它被应用于局部环境中的元素 x。然后,它将目标归约为 x 被每个构造子所取代的情况。
example (p : Nat → Prop)
(hz : p 0) (hs : ∀ n, p (Nat.succ n)) :
∀ n, p n := p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succ⊢ ∀ (n : Nat), p n
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succn:Nat⊢ p n
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succ⊢ p 0p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succn✝:Nat⊢ p (n✝ + 1)
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succ⊢ p 0 All goals completed! 🐙
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succn✝:Nat⊢ p (n✝ + 1) All goals completed! 🐙
在第一个分支中,证明状态是:
在第二个分支中,它是:
还有一些额外的修饰功能。首先,cases 允许你使用 with 子句来选择每个选项的名称。
例如在下一个例子中,我们为 succ 的参数选择 m 这个名字,这样第二个情况就指的是 succ m。
更重要的是,cases 策略将检测局部环境中任何依赖于目标变量的项目。它将这些元素还原,进行拆分,并重新引入它们。
在下面的例子中,注意假设 h : n ≠ 0 在第一个分支中变成 h : 0 ≠ 0,在第二个分支中变成 h : m + 1 ≠ 0。
open Nat
example (n : Nat) (h : n ≠ 0) : succ (pred n) = n := n:Nath:n ≠ 0⊢ n.pred.succ = n
cases n with
h:0 ≠ 0⊢ (pred 0).succ = 0
All goals completed! 🐙
m:Nath:m + 1 ≠ 0⊢ (m + 1).pred.succ = m + 1
All goals completed! 🐙
注意 cases 可以用来产生数据,也可以用来证明命题。
def f (n : Nat) : Nat := n:Nat⊢ Nat
⊢ Natn✝:Nat⊢ Nat; n✝:Nat⊢ Nat; All goals completed! 🐙
example : f 0 = 3 := rfl
example : f 5 = 7 := rfl
再一次,cases 将还原、拆分,然后在上下文中重新引入依赖关系。
def Tuple (α : Type) (n : Nat) :=
{ as : List α // as.length = n }
def f {n : Nat} (t : Tuple α n) : Nat := α:Typen:Natt:Tuple α n⊢ Nat
α:Typet:Tuple α 0⊢ Natα:Typen✝:Natt:Tuple α (n✝ + 1)⊢ Nat; α:Typen✝:Natt:Tuple α (n✝ + 1)⊢ Nat; All goals completed! 🐙
def myTuple : Tuple Nat 3 :=
⟨[0, 1, 2], rfl⟩
example : f myTuple = 7 :=
rfl
这是一个有多个带参数构造子的例子。
inductive Foo where
| bar1 : Nat → Nat → Foo
| bar2 : Nat → Nat → Nat → Foo
def silly (x : Foo) : Nat := x:Foo⊢ Nat
cases x with
a:Natb:Nat⊢ Nat All goals completed! 🐙
c:Natd:Nate:Nat⊢ Nat All goals completed! 🐙
每个构造子的选项不需要按照构造子声明的顺序来解决。
def silly (x : Foo) : Nat := x:Foo⊢ Nat
cases x with
c:Natd:Nate:Nat⊢ Nat All goals completed! 🐙
a:Natb:Nat⊢ Nat All goals completed! 🐙
with 的语法对于编写结构化证明很方便。
Lean 还提供了一个补充的 case 策略,它允许你专注于目标并分配变量名。
def silly (x : Foo) : Nat := x:Foo⊢ Nat
a✝¹:Nata✝:Nat⊢ Nata✝²:Nata✝¹:Nata✝:Nat⊢ Nat
case bar1 a b a:Natb:Nat⊢ Nat All goals completed! 🐙
case bar2 c d e c:Natd:Nate:Nat⊢ Nat All goals completed! 🐙
case 策略很聪明,它会将构造子匹配到适当的目标。例如,我们可以按相反的顺序填充上面的目标:
def silly (x : Foo) : Nat := x:Foo⊢ Nat
a✝¹:Nata✝:Nat⊢ Nata✝²:Nata✝¹:Nata✝:Nat⊢ Nat
case bar2 c d e c:Natd:Nate:Nat⊢ Nat All goals completed! 🐙
case bar1 a b a:Natb:Nat⊢ Nat All goals completed! 🐙
你也可以对任意表达式使用 cases。假设该表达式出现在目标中,cases 策略将对该表达式进行泛化,
引入结果的全称量化变量,并对其进行分情况讨论。
open Nat
example (p : Nat → Prop) (hz : p 0) (hs : ∀ n, p (succ n)) (m k : Nat)
: p (m + 3 * k) := p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Nat⊢ p (m + 3 * k)
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Nat⊢ p 0p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Natn✝:Nat⊢ p (n✝ + 1)
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Natn✝:Nat⊢ p (n✝ + 1) -- goal is p 0
All goals completed! 🐙 -- goal is a : Nat ⊢ p (succ a)
可以将其理解为“根据 m + 3 * k 是零还是某个数的后继来进行分情况讨论”。结果在功能上等同于以下内容:
open Nat
example (p : Nat → Prop) (hz : p 0) (hs : ∀ n, p (succ n)) (m k : Nat)
: p (m + 3 * k) := p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Nat⊢ p (m + 3 * k)
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Natn:Nat⊢ p n
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Nat⊢ p 0p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Natn✝:Nat⊢ p (n✝ + 1)
p:Nat → Prophz:p 0hs:∀ (n : Nat), p n.succm:Natk:Natn✝:Nat⊢ p (n✝ + 1)
All goals completed! 🐙
注意,表达式 m + 3 * k 被 generalize 擦除了;重要的是它是否为 0 或 n✝ + 1 的形式。
这种形式的 cases 不会 还原任何也提到方程中表达式(在本例中为 m + 3 * k)的假设。
如果这样的项出现在假设中,并且你也想对其进行泛化,你需要显式地 revert 它。
如果你进行分情况讨论的表达式没有出现在目标中,cases 策略会使用 have 将表达式的类型放入上下文中。
这是一个例子:
example (p : Prop) (m n : Nat)
(h₁ : m < n → p) (h₂ : m ≥ n → p) : p := p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → p⊢ p
p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → ph✝:m < n⊢ pp:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → ph✝:m ≥ n⊢ p
case inl hlt p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → phlt:m < n⊢ p All goals completed! 🐙
case inr hge p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → phge:m ≥ n⊢ p All goals completed! 🐙
定理 Nat.lt_or_ge m n 说 m < n ∨ m ≥ n,很自然地可以将上面的证明看作是针对这两种情况进行拆分。
在第一个分支中,我们有假设 hlt : m < n,在第二个分支中,我们有假设 hge : m ≥ n。
上面的证明在功能上等同于以下内容:
example (p : Prop) (m n : Nat)
(h₁ : m < n → p) (h₂ : m ≥ n → p) : p := p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → p⊢ p
p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → ph:m < n ∨ m ≥ n := Nat.lt_or_ge m n⊢ p
p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → ph✝:m < n⊢ pp:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → ph✝:m ≥ n⊢ p
case inl hlt p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → phlt:m < n⊢ p All goals completed! 🐙
case inr hge p:Propm:Natn:Nath₁:m < n → ph₂:m ≥ n → phge:m ≥ n⊢ p All goals completed! 🐙
在前两行之后,我们有 h : m < n ∨ m ≥ n 作为假设,我们只是对其进行分情况讨论。
这是另一个例子,我们利用自然数相等的可判定性来对 m = n 和 m ≠ n 的情况进行拆分。
#check Nat.sub_self
example (m n : Nat) : m - n = 0 ∨ m ≠ n := m:Natn:Nat⊢ m - n = 0 ∨ m ≠ n
cases Decidable.em (m = n) with
m:Natn:Natheq:m = n⊢ m - n = 0 ∨ m ≠ n m:Natn:Natheq:m = n⊢ n - n = 0 ∨ n ≠ n; m:Natn:Natheq:m = n⊢ n - n = 0; All goals completed! 🐙
m:Natn:Nathne:¬m = n⊢ m - n = 0 ∨ m ≠ n m:Natn:Nathne:¬m = n⊢ m ≠ n; All goals completed! 🐙
请记住,如果你 open Classical,你可以对任何命题使用排中律。
但是使用类型类推断(参见 类型类),Lean 实际上可以找到相关的判定过程,
这意味着你可以在可计算函数中使用情况拆分。
就像 cases 策略可以用来进行分情况证明一样,induction 策略可以用来进行归纳证明。
语法与 cases 类似,只是参数只能是局部上下文中的项。这是一个例子:
namespace Hidden
theorem zero_add (n : Nat) : 0 + n = n := n:Nat⊢ 0 + n = n
induction n with
⊢ 0 + 0 = 0 All goals completed! 🐙
n:Natih:0 + n = n⊢ 0 + (n + 1) = n + 1 All goals completed! 🐙
end Hidden
与 cases 一样,我们可以使用 case 策略来代替 with。
namespace Hidden
theorem zero_add (n : Nat) : 0 + n = n := n:Nat⊢ 0 + n = n
⊢ 0 + 0 = 0n✝:Nata✝:0 + n✝ = n✝⊢ 0 + (n✝ + 1) = n✝ + 1
case zero ⊢ 0 + 0 = 0 All goals completed! 🐙
case succ n ih n:Natih:0 + n✝ = n✝⊢ 0 + (n✝ + 1) = n✝ + 1 All goals completed! 🐙
end Hidden
这里还有一些额外的例子:
Nat.mod.inductionOn
{motive : Nat → Nat → Sort u}
(x y : Nat)
(ind : ∀ x y, 0 < y ∧ y ≤ x → motive (x - y) y → motive x y)
(base : ∀ x y, ¬(0 < y ∧ y ≤ x) → motive x y) :
motive x y
example (x : Nat) {y : Nat} (h : y > 0) : x % y < y := x:Naty:Nath:y > 0⊢ x % y < y
induction x, y using Nat.mod.inductionOn with
x:Naty:Nath₁:0 < y ∧ y ≤ xih:y > 0 → (x - y) % y < yh:y > 0⊢ x % y < y
x:Naty:Nath₁:0 < y ∧ y ≤ xih:y > 0 → (x - y) % y < yh:y > 0⊢ (x - y) % y < y
All goals completed! 🐙
x:Naty:Nath₁:¬(0 < y ∧ y ≤ x)h:y > 0⊢ x % y < y
x:Naty:Nath₁:¬(0 < y ∧ y ≤ x)h:y > 0this:¬0 < y ∨ ¬y ≤ x := Decidable.not_and_iff_or_not.mp h₁⊢ x % y < y
match this with
x:Naty:Nath₁✝:¬(0 < y ∧ y ≤ x)h:y > 0this:¬0 < y ∨ ¬y ≤ x := Decidable.not_and_iff_or_not.mp h₁h₁:¬0 < y⊢ x % y < y All goals completed! 🐙
x:Naty:Nath₁✝:¬(0 < y ∧ y ≤ x)h:y > 0this:¬0 < y ∨ ¬y ≤ x := Decidable.not_and_iff_or_not.mp h₁h₁:¬y ≤ x⊢ x % y < y
x:Naty:Nath₁✝:¬(0 < y ∧ y ≤ x)h:y > 0this:¬0 < y ∨ ¬y ≤ x := Decidable.not_and_iff_or_not.mp h₁h₁:¬y ≤ xhgt:y > x := Nat.gt_of_not_le h₁⊢ x % y < y
x:Naty:Nath₁✝:¬(0 < y ∧ y ≤ x)h:y > 0this:¬0 < y ∨ ¬y ≤ x := Decidable.not_and_iff_or_not.mp h₁h₁:¬y ≤ xhgt:y > x % y⊢ x % y < y
All goals completed! 🐙
你也可以在策略中使用 match 符号:
example : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p
p:Propq:Proph:p ∨ q⊢ q ∨ p
match h with
p:Propq:Proph:p ∨ qh✝:p⊢ q ∨ p p:Propq:Proph:p ∨ qh✝:p⊢ p; All goals completed! 🐙
p:Propq:Proph:p ∨ qh2:q⊢ q ∨ p p:Propq:Proph:p ∨ qh2:q⊢ q; All goals completed! 🐙
为了方便起见,模式匹配已集成到诸如 intro 和 funext 之类的策略中。
example : s ∧ q ∧ r → p ∧ r → q ∧ p := s:Propq:Propr:Propp:Prop⊢ s ∧ q ∧ r → p ∧ r → q ∧ p
intro ⟨_, ⟨hq, _⟩⟩ s:Propq:Propr:Propp:Propleft✝:shq:qright✝¹:rhp:pright✝:r⊢ q ∧ p
All goals completed! 🐙
example :
(fun (x : Nat × Nat) (y : Nat × Nat) => x.1 + y.2)
=
(fun (x : Nat × Nat) (z : Nat × Nat) => z.2 + x.1) := ⊢ (fun x y => x.fst + y.snd) = fun x z => z.snd + x.fst
a:Natb:Natc:Natd:Nat⊢ (a, b).fst + (c, d).snd = (c, d).snd + (a, b).fst
a:Natb:Natc:Natd:Nat⊢ a + d = d + a
All goals completed! 🐙
我们用最后一个旨在促进归纳类型工作的策略来结束本节,即 injection 策略。
根据设计,归纳类型的元素是自由生成的,也就是说,构造子是单射的并且具有不相交的值域。
injection 策略旨在利用这一事实:
open Nat
example (m n k : Nat) (h : succ (succ m) = succ (succ n))
: n + k = m + k := m:Natn:Natk:Nath:m.succ.succ = n.succ.succ⊢ n + k = m + k
m:Natn:Natk:Nath':m.succ = n.succ⊢ n + k = m + k
m:Natn:Natk:Nath'':m = n⊢ n + k = m + k
All goals completed! 🐙
该策略的第一个实例将 h' : m.succ = n.succ 添加到上下文中,第二个实例添加 h'' : m = n。
injection 策略还会检测当不同的构造子被设为相等时产生的矛盾,并使用它们来关闭目标。
open Nat
example (m n : Nat) (h : succ m = 0) : n = n + 7 := m:Natn:Nath:m.succ = 0⊢ n = n + 7
All goals completed! 🐙
example (m n : Nat) (h : succ m = 0) : n = n + 7 := m:Natn:Nath:m.succ = 0⊢ n = n + 7
All goals completed! 🐙
example (h : 7 = 4) : False := h:7 = 4⊢ False
All goals completed! 🐙
如第二个例子所示,contradiction 策略也会检测这种形式的矛盾。
7.7. 归纳族
我们几乎已经描述完了 Lean 接受的所有归纳定义。到目前为止,你已经看到 Lean 允许你引入具有任意数量递归构造子的归纳类型。 事实上,单个归纳定义可以引入一个索引的归纳类型 族,我们现在将描述这种方式。
归纳族是由以下形式的同时归纳定义的索引类型族:
inductive foo : ... → Sort u where | constructor₁ : ... → foo ... | constructor₂ : ... → foo ... ... | constructorₙ : ... → foo ...
inductive Vect (α : Type u) : Nat → Type u where
| nil : Vect α 0
| cons : α → {n : Nat} → Vect α n → Vect α (n + 1)
注意,cons 构造子接收 Vect α n 的一个元素,并返回 Vect α (n + 1) 的一个元素,从而使用家族中的一个成员的元素来构建另一个成员的元素。
一个更奇特的例子是由 Lean 中相等类型的定义:
对于每个固定的 α : Sort u 和 a : α,这个定义构造了一个类型族 Eq a x,由 x : α 索引。
值得注意的是,只有一个构造子 refl,它是 Eq a a 的一个元素。
直观地说,构造 Eq a x 的证明的唯一方法是使用自反性,即在 x 为 a 的情况下。
请注意,Eq a a 是类型族 Eq a x 中唯一有居留元的类型。Lean 生成的消去原理如下:
set_option pp.proofs true
universe u v
#check (@Eq.rec : {α : Sort u} → {a : α} →
{motive : (x : α) → a = x → Sort v} →
motive a rfl →
{b : α} → (h : a = b) → motive b h)
一个显著的事实是,所有关于相等的基本公理都来自构造子 refl 和消去器 Eq.rec。
然而,相等的定义是不典型的,参见 公理化细节 一节的讨论。
递归器 Eq.rec 也被用来定义代换:
namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop}
(h₁ : Eq a b) (h₂ : p a) : p b :=
Eq.rec (motive := fun x _ => p x) h₂ h₁
end Hidden
你也可以使用 match 定义 subst。
namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop}
(h₁ : Eq a b) (h₂ : p a) : p b :=
match h₁ with
| rfl => h₂
end Hidden
实际上,Lean 使用基于生成的辅助函数(如 Eq.casesOn 和 Eq.ndrec)的定义来编译 match 表达式,
而这些辅助函数本身是使用 Eq.rec 定义的。
namespace Hidden
theorem subst {α : Type u} {a b : α} {p : α → Prop}
(h₁ : a = b) (h₂ : p a) : p b :=
match h₁ with
| rfl => h₂
set_option pp.all true
#print subst
#print subst.match_1_1
#print Eq.casesOn
#print Eq.ndrecend Hidden
使用递归器或 match 处理 h₁ : a = b,我们可以假设 a 和 b 是相同的,
在这种情况下,p b 和 p a 是相同的。
证明 Eq 是对称的和传递的并不难。
在下面的例子中,我们证明 symm,并将定理 trans 和 congr(同余)留作练习。
namespace Hidden
variable {α β : Type u} {a b c : α}
theorem symm (h : Eq a b) : Eq b a :=
match h with
| rfl => rfl
theorem trans (h₁ : Eq a b) (h₂ : Eq b c) : Eq a c :=
sorry
theorem congr (f : α → β) (h : Eq a b) : Eq (f a) (f b) :=
sorry
end Hidden
在类型论文献中,有对归纳定义的进一步推广,例如,归纳-递归(induction-recursion) 和 归纳-归纳(induction-induction) 的原则。这些 Lean 并不支持。
7.8. 公理化细节
我们已经通过例子描述了归纳类型和它们的语法。本节为那些对公理基础感兴趣的人提供额外的信息。
我们已经看到,归纳类型的构造子需要 参量(parameters),即在整个归纳构造过程中保持固定的参数,和 索引(indices),即参数化同时在构造中的类型族的参数。 每个构造子都应该有一个类型,其中的参数类型是由先前定义的类型、参量和索引类型以及当前正在定义的归纳族建立起来的。 要求是,如果后者存在,它只 严格正向(strictly positively) 出现。 简单来说,这意味着它所出现的构造子的任何参数都是一个依赖箭头类型,其中定义中的归纳类型只作为结果类型出现, 其中的索引是以常量和先前的参数来给出。
既然一个归纳类型对于某些 u 来说存在于 Sort u 中,那么我们有理由问 u 可以被实例化为 哪些 宇宙层级。
归纳类型族 C 的定义中的每个构造子 c 的形式为
c : (a : α) → (b : β[a]) → C a p[a,b]
其中 a 是一列数据类型的参量,b 是一列构造子的参数,p[a, b] 是索引,用于确定构造所处的归纳族的元素。
(请注意,这种描述有些误导,因为构造子的参数可以以任何顺序出现,只要依赖关系是合理的。)
对 C 的宇宙层级的约束分为两种情况,取决于归纳类型是否被指定落在 Prop(即 Sort 0)。
我们首先考虑归纳类型 不 指定落在 Prop 的情况。那么宇宙层级 u 被限制为满足以下条件:
对于上面的每个构造子
c,以及序列β[a]中的每个βk[a],如果βk[a] : Sort v,我们有u≥v。
换句话说,宇宙层级 u 被要求至少与代表构造子参数的每个类型的宇宙层级一样大。
当归纳类型被指定落在 Prop 中时,对构造子参数的宇宙层级没有任何限制。但是这些宇宙层级对消去规则有影响。
一般来说,对于 Prop 中的归纳类型,消去规则的 motive 被要求在 Prop 中。
这最后一条规则有一个例外:当只有一个构造子,并且每个构造子参数都在 Prop 中或者是一个索引时,
我们可以从一个归纳定义的 Prop 消去到一个任意的 Sort。
直观地说,在这种情况下,消去并不利用任何信息,而这些信息并不是由参数类型有居留元这一简单的事实所提供的。
这种特殊情况被称为 单子消去(singleton elimination)。
我们已经在 Eq.rec 的应用中看到了单子消去的作用,这是归纳定义的相等类型的消去器。
我们可以使用一个元素 h : Eq a b 来将一个元素 h₂ : p a 转换为 p b,
即使 p a 和 p b 是任意类型,因为转换并不产生新的数据;它只是重新解释了我们已经有的数据。
单子消去也用于异质相等和良基递归,这将在 归纳和递归 一章中讨论。
7.9. 相互和嵌套的归纳类型
我们现在考虑两个经常有用的归纳类型的推广,Lean 通过“编译”它们来支持上述更原始的归纳类型种类。 换句话说,Lean 解析了更一般的定义,在此基础上定义了辅助的归纳类型,然后使用辅助类型来定义我们真正想要的类型。 下一章将介绍 Lean 的方程编译器,它需要有效地利用这些类型。 尽管如此,在这里描述这些声明还是有意义的,因为它们是普通归纳定义的直接变形。
首先,Lean 支持 相互定义(mutually defined) 的归纳类型。这个想法是,我们可以同时定义两个(或更多)归纳类型,其中每个类型都指代其他类型。
mutual
inductive Even : Nat → Prop where
| even_zero : Even 0
| even_succ : (n : Nat) → Odd n → Even (n + 1)
inductive Odd : Nat → Prop where
| odd_succ : (n : Nat) → Even n → Odd (n + 1)
end
在这个例子中,同时定义了两种类型:一个自然数 n 如果是 0 或比 Odd 数多一个,就是 Even;
如果是比 Even 数多一个,就是 Odd。在下面的练习中,要求你写出细节。
相互的归纳定义也可以用来定义有限树的表示法,节点由 α 的元素标记:
mutual
inductive Tree (α : Type u) where
| node : α → TreeList α → Tree α
inductive TreeList (α : Type u) where
| nil : TreeList α
| cons : Tree α → TreeList α → TreeList α
end
有了这个定义,我们可以通过给出一个 α 的元素和一个子树列表(可能是空的)来构造 Tree α 的元素。
子树列表由 TreeList α 类型表示,它被定义为空列表 nil,或者是一棵树的 cons 和 TreeList α 的一个元素。
然而,这个定义在工作中是不方便的。如果子树的列表是由 List (Tree α) 类型给出的,那就更好了,
尤其是 Lean 的库中包含了一些处理列表的函数和定理。
我们可以证明 TreeList α 类型与 List (Tree α) 是 同构(isomorphic) 的,
但是沿着这个同构关系来回翻译结果是很乏味的。
事实上,Lean 允许我们定义我们真正想要的归纳类型:
inductive Tree (α : Type u) where
| mk : α → List (Tree α) → Tree α
这就是所谓的 嵌套(nested) 归纳类型。它不属于上一节给出的归纳类型的严格规范,
因为 Tree 不是严格正向地出现在 mk 的参数中,而是嵌套在 List 类型构造子中。
然后 Lean 在其内核中自动建立了 TreeList α 和 List (Tree α) 之间的同构关系,
并根据同构关系定义了 Tree 的构造子。
7.10. 练习
-
尝试定义自然数的其他运算,如乘法、前继函数(定义
pred 0 = 0)、 截断减法(当m大于或等于n时,n - m = 0)和乘方。 然后在我们已经证明的定理的基础上,尝试证明它们的一些基本属性。
由于其中许多已经在 Lean 的核心库中定义,你应该在一个名为 Hidden 或类似的命名空间中工作,以避免名称冲突。
-
定义一些对列表的操作,如
length函数或reverse函数。证明一些属性,比如下面这些:
a. length (xs ++ ys) = length xs + length ys
b. length (reverse xs) = length xs
c. reverse (reverse xs) = xs
-
定义一个归纳数据类型,由以下构造子建立的项组成:
递归地定义一个函数,根据变量的赋值来计算任何这样的项。
-
同样,定义命题公式的类型,以及关于这类公式类型的函数:求值函数、衡量公式复杂性的函数,以及用另一个公式替代给定变量的函数。