Theorem Proving in Lean 4

9. 结构体(Structure)与记录(Record)🔗

我们已经看到,Lean 的基础系统包含归纳类型(inductive type)。此外,我们注意到一个非凡的事实:仅凭类型宇宙(universe)、依值箭头类型(dependent arrow type)和归纳类型,就有可能构建起庞大的数学大厦;其他一切都由此派生。Lean 标准库包含了许多归纳类型的实例(例如 NatProdList),甚至逻辑连接词也是用归纳类型定义的。

回顾一下,只包含一个构造器(constructor)的非递归归纳类型称为结构体(structure)或记录(record)。积类型是一种结构体,依值积(Sigma)类型也是如此。一般来说,每当我们定义一个结构体 S,我们通常会定义投影函数(projection),允许我们"析构(destruct)"每个 S 的实例并检索其字段(field)中存储的值。函数 Prod.fstProd.snd 返回一对的第一个和第二个元素,就是此类投影的例子。

在编写程序或形式化数学时,定义包含许多字段的结构体并不罕见。Lean 提供的 structure 命令为此过程提供了基础设施。当我们使用此命令定义结构体时,Lean 会自动生成所有的投影函数。structure 命令还允许我们基于先前定义的结构体来定义新的结构体。此外,Lean 还为定义给定结构体的实例提供了便捷的表示法。

9.1. 声明结构体(Declaring Structures)🔗

structure 命令本质上是一个用于定义归纳数据类型的"前端(front end)"。每个 structure 声明都会引入一个同名命名空间(namespace)。一般形式如下:

    structure <name> <parameters> <parent-structures> where
      <constructor> :: <fields>

大多数部分是可选的。下面是一个例子:

structure Point (α : Type u) where mk :: x : α y : α

类型 Point 的值使用 Point.mk a b 创建,点 p 的字段使用 Point.x pPoint.y p 访问(但 p.xp.y 也可以,见下文)。structure 命令还会生成有用的递归子(recursor)和定理。以下是为上述声明生成的一些构造。

-- a Type Point.{u} (α : Type u) : Type u#check Point
Point.{u} (α : Type u) : Type u
-- the eliminator @Point.rec : {α : Type u_2} {motive : Point α Sort u_1} ((x y : α) motive { x := x, y := y }) (t : Point α) motive t#check @Point.rec
@Point.rec : {α : Type u_2} 
  {motive : Point α  Sort u_1}  ((x y : α)  motive { x := x, y := y })  (t : Point α)  motive t
-- the constructor @Point.mk : {α : Type u_1} α α Point α#check @Point.mk
@Point.mk : {α : Type u_1}  α  α  Point α
-- a projection @Point.x : {α : Type u_1} Point α α#check @Point.x
@Point.x : {α : Type u_1}  Point α  α
-- a projection @Point.y : {α : Type u_1} Point α α#check @Point.y
@Point.y : {α : Type u_1}  Point α  α

如果未提供构造器名称,则构造器默认命名为 mk

以下是一些使用所生成构造的简单定理和表达式。通常,你可以使用 open Point 命令来避免前缀 Point

10#eval Point.x (Point.mk 10 20)
10
20#eval Point.y (Point.mk 10 20)
20
open Point example (a b : α) : x (mk a b) = a := rfl example (a b : α) : y (mk a b) = b := rfl

给定 p : Point Nat,点表示法(dot notation)p.xPoint.x p 的简写。这提供了一种访问结构体字段的便捷方式。

def p := Point.mk 10 20 p.x : Nat#check p.x
p.x : Nat
10#eval p.x
10
20#eval p.y
20

点表示法不仅方便访问记录的投影,也方便应用在具有相同名称的命名空间中定义的函数。回顾合取(Conjunction)一节,如果 p 的类型是 Point,表达式 p.foo 被解释为 Point.foo p,前提是 foo 的第一个非隐式参数的类型为 Point。因此,在下面的例子中,表达式 p.add qPoint.add p q 的简写。

structure Point (α : Type u) where x : α y : α deriving Repr def Point.add (p q : Point Nat) := mk (p.x + q.x) (p.y + q.y) def p : Point Nat := Point.mk 1 2 def q : Point Nat := Point.mk 3 4 { x := 4, y := 6 }#eval p.add q
{ x := 4, y := 6 }

在下一章中,你将学习如何定义像 add 这样的函数,使其适用于 Point α 的元素而不只是 Point Nat,前提是 α 具有关联的加法运算。

更一般地,给定表达式 p.foo x y z,其中 p : Point,Lean 会将 p 插入到 Point.foo 的第一个类型为 Point 的参数位置。例如,使用下面的标量乘法定义,p.smul 3 被解释为 Point.smul 3 p

def Point.smul (n : Nat) (p : Point Nat) := Point.mk (n * p.x) (n * p.y) def p : Point Nat := Point.mk 1 2 { x := 3, y := 6 }#eval p.smul 3
{ x := 3, y := 6 }
example {p : Point Nat} : p.smul 3 = Point.smul 3 p := rfl

使用 List.map 函数(它将列表作为其第二个非隐式参数)采用类似的技巧是很常见的:

@List.map : {α : Type u_1} {β : Type u_2} (α β) List α List β#check @List.map
@List.map : {α : Type u_1}  {β : Type u_2}  (α  β)  List α  List β
def xs : List Nat := [1, 2, 3] def f : Nat Nat := fun x => x * x [1, 4, 9]#eval xs.map f
[1, 4, 9]
example {xs : List α} {f : α β} : xs.map f = List.map f xs := rfl

这里 xs.map f 被解释为 List.map f xs

9.2. 对象(Objects)🔗

我们一直在使用构造器来创建结构体类型的元素。对于包含许多字段的结构体来说,这通常很不方便,因为我们必须记住字段定义的顺序。因此,Lean 提供了以下替代表示法来定义结构体类型的元素。

    { (<field-name> := <expr>)* : structure-type }
    or
    { (<field-name> := <expr>)* }

后缀 : structure-type 可以在结构体名称能从期望类型推断出时省略。例如,我们使用这种表示法来定义"点"。字段指定的顺序无关紧要,因此下面的所有表达式都定义同一个点。

structure Point (α : Type u) where x : α y : α { x := 10, y := 20 } : Point Nat#check { x := 10, y := 20 : Point Nat }
{ x := 10, y := 20 } : Point Nat
{ x := 10, y := 20 } : Point Nat#check { y := 20, x := 10 : Point _ }
{ x := 10, y := 20 } : Point Nat
{ x := 10, y := 20 } : Point Nat#check ({ x := 10, y := 20 } : Point Nat)
{ x := 10, y := 20 } : Point Nat
example : Point Nat := { y := 20, x := 10 }

字段可以使用花括号标记为隐式(implicit)。隐式字段会成为构造器的隐式参数。

如果未指定字段的值,Lean 会尝试推断它。如果无法推断未指定的字段,Lean 会标记一个错误,指示相应的占位符无法被合成(synthesize)。

structure MyStruct where {α : Type u} {β : Type v} a : α b : β { α := Nat, β := Bool, a := 10, b := true } : MyStruct#check { a := 10, b := true : MyStruct }
{ α := Nat, β := Bool, a := 10, b := true } : MyStruct

记录更新(record update)是另一种常见操作,即通过修改旧记录中一个或多个字段的值来创建新的记录对象。Lean 允许你通过添加注解 s with 到字段赋值之前,来指定记录的规范中未赋值的字段应从先前定义的结构体对象 s 中获取。如果提供了多个记录对象,则按顺序访问它们,直到 Lean 找到包含未指定字段的那个。如果在访问所有对象后仍有任何字段名未指定,Lean 会引发错误。

structure Point (α : Type u) where x : α y : α deriving Repr def p : Point Nat := { x := 1, y := 2 } { x := 1, y := 3 }#eval { p with y := 3 }
{ x := 1, y := 3 }
{ x := 4, y := 2 }#eval { p with x := 4 }
{ x := 4, y := 2 }
structure Point3 (α : Type u) where x : α y : α z : α def q : Point3 Nat := { x := 5, y := 5, z := 5 } def r : Point3 Nat := { p, q with x := 6 } example : r.x = 6 := rfl example : r.y = 2 := rfl example : r.z = 5 := rfl

9.3. 继承(Inheritance)🔗

我们可以通过添加新字段来扩展(extend)现有结构体。这一特性使我们能够模拟一种继承(inheritance)。

structure Point (α : Type u) where x : α y : α inductive Color where | red | green | blue structure ColorPoint (α : Type u) extends Point α where c : Color

在下一个例子中,我们使用多重继承定义一个结构体,然后使用父结构体的对象来定义一个对象。

structure Point (α : Type u) where x : α y : α z : α structure RGBValue where red : Nat green : Nat blue : Nat structure RedGreenPoint (α : Type u) extends Point α, RGBValue where no_blue : blue = 0 def p : Point Nat := { x := 10, y := 10, z := 20 } def rgp : RedGreenPoint Nat := { p with red := 200, green := 40, blue := 0, no_blue := rfl } example : rgp.x = 10 := rfl example : rgp.red = 200 := rfl