7. 归纳类型(Inductive Types)
我们已经看到,Lean 的形式化基础包括基本类型、Prop、Type 1、Type 2……,并允许形成依值函数类型(dependent function type),(x : α) → β。在示例中,我们还使用了其他类型,如 Bool、Nat 和 Int,以及类型构造子(type constructor),如 List、积类型 ×。事实上,在 Lean 的库中,除宇宙(universe)之外的每一个具体类型,以及除依值箭头(dependent arrow)之外的每一个类型构造子,都是被称为归纳类型(inductive type)的一般类型构造家族的实例。值得注意的是,仅凭类型宇宙、依值箭头类型和归纳类型,就可以构建起实质性的数学大厦;其他一切都源于这些。
直观地说,归纳类型是由一个指定的构造子(constructor)列表构建而成的。在 Lean 中,指定这样的类型的语法如下:
inductive Foo where | constructor₁ : ... → Foo | constructor₂ : ... → Foo ... | constructorₙ : ... → Foo
其直觉是每个构造子指定了一种构建 Foo 新对象的方法,可能基于先前构建的值。Foo 类型仅由以这种方式构造的对象组成。
我们将在下面看到,构造子的参数可以包括 Foo 类型的对象,但须满足某种“正性(positivity)”约束,这保证了 Foo 的元素是从底层向上构建的。粗略地说,每个 … 可以是由 Foo 和先前定义的类型构造的任意箭头类型(arrow type),其中 Foo 如果出现,则仅作为依值箭头类型(dependent arrow type)的“目标(target)”。
我们将提供许多归纳类型的示例。我们还将考虑上述方案的轻微推广,即相互定义的归纳类型(mutually defined inductive type)和所谓的归纳族(inductive family)。
与逻辑连接词一样,每个归纳类型都带有引入规则(introduction rule),它展示了如何构造该类型的元素,以及消去规则(elimination rule),它展示了如何在另一个构造中“使用”该类型的元素。与逻辑连接词的类比不应令人惊讶;正如我们将在下面看到的,它们本身也是归纳类型构造的实例。你已经看到了归纳类型的引入规则:它们就是在类型定义中指定的构造子。消去规则提供了类型上的递归原理(principle of recursion),其中包括归纳原理(principle of induction)作为一个特例。
在下一章中,我们将描述 Lean 的函数定义包,它提供了在归纳类型上定义函数和进行归纳证明的更便捷的方式。但由于归纳类型的概念如此基础,我们认为从低层次的、动手实践的理解开始是很重要的。我们将从一些归纳类型的基本示例开始,逐步深入到更复杂和精巧的示例。
7.1. 枚举类型(Enumerated Types)
最简单的归纳类型是具有有限枚举元素列表的类型。
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 表达式使用声明归纳类型时生成的递归子 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
使用策略证明(tactic proof),我们可以更简洁:
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! 🐙
下面的Tactics for Inductive Types将介绍专门设计用于利用归纳类型的其他策略。
注意,在命题即类型(propositions-as-types)对应下,我们可以使用 match 来证明定理以及定义函数。换句话说,在命题即类型对应下,按情况证明(proof by cases)是一种按情况定义(definition by cases),其中被“定义”的是证明而不是数据。
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. 带参数的构造子(Constructors with Arguments)
枚举类型是归纳类型的一个非常特殊的情况,其中构造子根本不带参数。通常,一个“构造”可以依赖于数据,这些数据随后在构造的参数中体现。考虑库中积类型(product type)和和类型(sum type)的定义:
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 函数是一个布尔条件式:cond b t1 t2 在 b 为真时返回 t1,否则返回 t2。函数 prod_example 接受一个由布尔值 b 和数字 n 组成的序对,并根据 b 是真还是假返回 2 * n 或 2 * n + 1。
相比之下,和类型有两个构造子 inl 和 inr(分别表示“insert left”和“insert right”),每个接受一个(显式)参数。要定义 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 检测到这些参数可以从构造子的后续参数或返回类型中推断出来时,就会使它们成为隐式参数。
在Defining the Natural Numbers中,我们将看到当归纳类型的构造子接受来自该归纳类型本身的参数时会发生什么。我们本节考虑示例的特点是每个构造子仅依赖于先前指定的类型。
注意,具有多个构造子的类型是析取性的:Sum α β 的元素要么是 inl a 或 inr b。具有多个参数的构造子引入合取性信息:从 Prod.mk a b 这样的 Prod α β 元素中,我们可以提取 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 这样只有一个构造子的类型是纯合取性的:构造子简单地将参数列表打包成单一数据,本质上是一个元组(tuple),其中后续参数的类型可以依赖于初始参数的类型。我们也可以将这样的类型视为“记录(record)”或“结构体(structure)”。在 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 yellowyellow 的定义使用所示的三个值构成记录,投影 Color.red 返回红色分量。
structure 命令对于定义代数结构特别有用,Lean 为此提供了大量的基础设施。例如,以下是半群(semigroup)的定义:
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)
我们将在关于结构体与记录(structures and records)的章节中看到更多示例。
我们已经讨论过依值积类型(dependent product type)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
在依值类型论的语义中,没有内建的偏函数(partial function)概念。函数类型 α → β 或依值函数类型 (a : α) → β 的每个元素都被假定在每个输入处都有一个值。Option 类型提供了一种表示偏函数的方式。Option β 的元素要么是 none,要么是形如 some b(对于某个值 b : β)。因此,我们可以将类型 α → Option β 的一个元素 f 看作从 α 到 β 的偏函数:对于每个 a : α,f a 要么返回 none(表示 f a “未定义”),要么返回 some b。
Inhabited α 的一个元素只是 α 中存在一个元素的证据。稍后,我们将看到 Inhabited 是 Lean 中类型类(type class)的一个例子:可以指示 Lean 哪些基础类型是 inhabited 的,Lean 可以在此基础上自动推断其他构造的类型是 inhabited 的。
作为练习,我们鼓励你为从 α 到 β 和从 β 到 γ 的偏函数开发一个复合(composition)概念,并证明其行为符合预期。我们还鼓励你证明 Bool 和 Nat 是 inhabited 的,两个 inhabited 类型的积类型是 inhabited 的,以及到 inhabited 类型的函数类型是 inhabited 的。
7.3. 归纳定义的命题(Inductively Defined Propositions)
归纳定义的类型可以位于任何类型宇宙中,包括最底层的 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 不携带任何数据。然而,这条规则有一个小例外,我们将在下面的Inductive Families中讨论。
甚至存在量词(existential quantifier)也是归纳定义的:
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 是使用结构体命令定义的:
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. 定义自然数(Defining the Natural Numbers)
到目前为止,我们看到的归纳定义类型都是“平坦的”:构造子包装数据并将其插入类型中,相应的递归子解包数据并对其进行操作。当构造子作用于正在定义的类型本身的元素时,情况就变得有趣得多了。一个典型的例子是自然数类型 Nat:
有两个构造子。我们从 zero : Nat 开始;它不接受参数,所以我们从一开始就有了它。相比之下,构造子 succ 只能应用于先前构造的 Nat。将其应用于 zero 得到 succ zero : Nat。再次应用得到 succ (succ zero) : Nat,依此类推。直观上,Nat 是包含这些构造子的“最小”类型,意味着它是由从 zero 开始并重复应用 succ 穷尽地(且自由地)生成的。
与之前一样,Nat 上的递归子旨在定义从 Nat 到任意值域(domain)的依值函数 f,即对于某个 motive : Nat → Sort u,是 (n : Nat) → motive n 的一个元素。它必须处理两种情况:输入为 zero 的情况,以及对某个 n : Nat 输入形如 succ n 的情况。在第一种情况下,我们像之前一样简单地指定一个具有适当类型的目标值。然而,在第二种情况下,递归子可以假设 f 在 n 处的值已经被计算出来了。因此,递归子的下一个参数根据 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 是所定义函数的余值域(codomain)。在类型论中,通常称 motive 为该消去/递归的动机(motive),因为它描述了我们想要构造的对象种类。接下来的两个参数如上所述指定了如何处理零和后继情况。它们也被称为次要前提(minor premises)。最后,t : Nat 是函数的输入。它也被称为主要前提(major premise)。
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
我们将在Type Classes一章中解释 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. 其他递归数据类型(Other Recursive Data Types)
让我们考虑更多归纳定义类型的例子。对于任意类型 α,α 的元素组成的列表(list)类型 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 通常称为列表的“头部(head)”,剩余部分 t 称为“尾部(tail)”。
作为练习,证明以下命题:
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)。
另一个例子,我们可以定义二叉树(binary tree)的类型:
inductive BinaryTree where
| leaf : BinaryTree
| node : BinaryTree → BinaryTree → BinaryTree
事实上,我们甚至可以定义可数分支树(countably branching tree)的类型:
7.6. 面向归纳类型的策略(Tactics for Inductive Types)
鉴于归纳类型在 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。更重要的是,cases 策略会检测局部上下文中依赖于目标变量的任何项。它会撤回这些元素,进行分裂,然后重新引入它们。
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 策略将对表达式进行泛化(generalize),引入所得的全称量化变量,然后对其分情况。
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 不会撤回也在该等式中提到该表达式的任何假设。如果这样的项出现在假设中并且你也想对其进行泛化,则需要显式地 revert 它。
如果你进行 case 分析的表达式没有出现在目标中,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 作为假设,然后我们对该假设进行 cases。
这是另一个例子,我们使用自然数上相等的可判定性来分裂为 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,你可以对任意命题使用排中律。但通过类型类推断(参见Type Classes),Lean 实际上可以找到相关的判定过程,这意味着你可以在可计算函数中使用 case 分裂。
正如 cases 策略可以用于执行按情况证明(proof by cases),induction 策略可以用于执行归纳证明(proof by 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
以下是一些额外的示例:
namespace Hidden
inductive Nat where
| zero
| succ : Nat → Nat
def Nat.toNat : Nat → _root_.Nat
| .zero => .zero
| .succ n => .succ n.toNat
def Nat.ofNat : _root_.Nat → Nat
| .zero => .zero
| .succ n => .succ (.ofNat n)
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
instance : OfNat Nat n where
ofNat := .ofNat n
@[simp]
theorem zero_zero : (.zero : Nat) = 0 := rfl
theorem add_zero (n : Nat) : n + 0 = n := rfl
theorem add_succ (n k : Nat) : n + k.succ = (n + k).succ := rfl
open Nat
theorem zero_add (n : Nat) : 0 + n = n := n:Nat⊢ 0 + n = n
⊢ 0 + zero = zeroa✝:Nata_ih✝:0 + a✝ = a✝⊢ 0 + a✝.succ = a✝.succ ⊢ 0 + zero = zeroa✝:Nata_ih✝:0 + a✝ = a✝⊢ 0 + a✝.succ = a✝.succ All goals completed! 🐙
theorem succ_add (m n : Nat) : succ m + n = succ (m + n) := m:Natn:Nat⊢ m.succ + n = (m + n).succ
m:Nat⊢ m.succ + zero = (m + zero).succm:Nata✝:Nata_ih✝:m.succ + a✝ = (m + a✝).succ⊢ m.succ + a✝.succ = (m + a✝.succ).succ m:Nat⊢ m.succ + zero = (m + zero).succm:Nata✝:Nata_ih✝:m.succ + a✝ = (m + a✝).succ⊢ m.succ + a✝.succ = (m + a✝.succ).succ All goals completed! 🐙
theorem add_comm (m n : Nat) : m + n = n + m := m:Natn:Nat⊢ m + n = n + m
m:Nat⊢ m + zero = zero + mm:Nata✝:Nata_ih✝:m + a✝ = a✝ + m⊢ m + a✝.succ = a✝.succ + m m:Nat⊢ m + zero = zero + mm:Nata✝:Nata_ih✝:m + a✝ = a✝ + m⊢ m + a✝.succ = a✝.succ + m All goals completed! 🐙
theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) := m:Natn:Natk:Nat⊢ m + n + k = m + (n + k)
m:Natn:Nat⊢ m + n + zero = m + (n + zero)m:Natn:Nata✝:Nata_ih✝:m + n + a✝ = m + (n + a✝)⊢ m + n + a✝.succ = m + (n + a✝.succ) m:Natn:Nat⊢ m + n + zero = m + (n + zero)m:Natn:Nata✝:Nata_ih✝:m + n + a✝ = m + (n + a✝)⊢ m + n + a✝.succ = m + (n + a✝.succ) All goals completed! 🐙
end Hidden
induction 策略也支持用户自定义的具有多个目标(即多个主要前提)的归纳原理。
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. 归纳族(Inductive Families)
我们几乎完成了对 Lean 接受的归纳定义的全部范围的描述。到目前为止,你已经看到 Lean 允许你引入具有任意数量的递归构造子的归纳类型。事实上,单个归纳定义可以引入一个带索引的族的归纳类型,方式如下所述。
归纳族是通过以下形式的同步归纳(simultaneous induction)定义的一个带索引的类型族:
inductive foo : ... → Sort u where | constructor₁ : ... → foo ... | constructor₂ : ... → foo ... ... | constructorₙ : ... → foo ...
与构造某个 Sort u 的元素的普通归纳定义相比,更一般的版本构造一个函数 … → Sort u,其中“…”表示一系列参数类型,也称为索引(indices)。然后每个构造子构造该族的某个成员的元素。一个例子是 Vect α n 的定义,即长度为 n 的 α 元素向量(vector)的类型:
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 : α,该定义构造了一个由 α 中的 x 索引的类型族 Eq a x。但值得注意的是,只有一个构造子 refl,它是 Eq a a 的一个元素。直观上,构造 Eq a x 的证明的唯一方式是使用自反性(reflexivity),即当 x 就是 a 时。注意 Eq a a 是类型族 Eq a x 中唯一 inhabited 的类型。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,这是一个显著的事实。然而,等式的定义是非典型的;请参阅Axiomatic Details中的讨论。
递归子 Eq.rec 也用于定义替换(substitution):
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. 公理细节(Axiomatic Details)
我们通过示例描述了归纳类型及其语法。本节为对公理基础感兴趣的读者提供补充信息。
我们已经看到,归纳类型的构造子接受参数(parameters)——直观上,在整个归纳构造过程中保持固定的参数——和索引(indices),即参数化同时正在构造的类型族的参数。每个构造子应具有一个类型,其中参数类型由先前定义的类型、参数和索引类型以及当前定义的归纳族构建。要求是,如果后者出现,则只能以严格正(strictly positive)的方式出现。这意味着,任何包含它的构造子参数都是一个依值箭头类型,其中正在定义的归纳类型仅作为结果类型出现,且索引由常数和先前参数给出。
由于归纳类型位于某个 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。其直觉是,在这种情况下,消去不会使用任何未由参数类型被 inhabited 这一单纯事实所给出的信息。这种特殊情况称为单例消去(singleton elimination)。
我们已经看到单例消去在 Eq.rec 的应用中发挥了作用,Eq.rec 是归纳定义等式类型的消去子。我们可以使用 h : Eq a b 来将 h₂ : p a 转换为 p b,即使 p a 和 p b 是任意类型,因为转换不会产生新数据;它只是重新解释我们已经拥有的数据。单例消去也用于异质等式和良基递归,这将在归纳与递归一章中讨论。
7.9. 相互定义和嵌套的归纳类型(Mutual and Nested Inductive Types)
我们现在考虑归纳类型的两个推广,它们通常很有用,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 是 Even(偶数),如果它是 0 或比某个 Odd(奇数)数大 1;它是 Odd,如果它比某个 Even 数大 1。在下面的练习中,你将被要求阐明细节。
相互归纳定义也可以用来定义以 α 元素标记节点的有限树的记法:
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 α) 是同构的,但沿着同构来回转换结果很繁琐。
事实上,Lean 允许我们定义我们真正想要的归纳类型:
inductive Tree (α : Type u) where
| mk : α → List (Tree α) → Tree α
这被称为嵌套的(nested)归纳类型。它超出了上一节给出的归纳类型的严格规范,因为 Tree 没有严格正地出现在 mk 的参数中,而是嵌套在 List 类型构造子内部。Lean 然后在其内核中自动构建 TreeList α 和 List (Tree α) 之间的同构,并基于该同构定义 Tree 的构造子。
7.10. 练习(Exercises)
-
尝试在自然数上定义其他运算,例如乘法、前驱函数(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
-
定义一个归纳数据类型,由以下构造子构成的项组成:
-
const n,一个表示自然数 n 的常量
-
var n,一个变量,编号为 n
-
plus s t,表示 s 和 t 的和
-
times s t,表示 s 和 t 的积
递归地定义一个函数,该函数根据变量的赋值求值任何这样的项。
-
-
类似地,定义命题公式的类型,以及该类型上的函数:求值函数、测量公式复杂度的函数、以及用另一个公式替换给定变量的函数。