9. 结构体(Structure)与记录(Record)9. Structures and Records🔗
我们已经看到,Lean 的基础系统包含归纳类型(inductive type)。此外,我们注意到一个非凡的事实:仅凭类型宇宙(universe)、依值箭头类型(dependent arrow type)和归纳类型,就有可能构建起庞大的数学大厦;其他一切都由此派生。Lean 标准库包含了许多归纳类型的实例(例如 Nat、Prod、List),甚至逻辑连接词也是用归纳类型定义的。We have seen that Lean's foundational system includes inductive
types. We have, moreover, noted that it is a remarkable fact that it
is possible to construct a substantial edifice of mathematics based on
nothing more than the type universes, dependent arrow types, and inductive types;
everything else follows from those. The Lean standard library contains
many instances of inductive types (e.g., Nat, Prod, List),
and even the logical connectives are defined using inductive types.
回顾一下,只包含一个构造器(constructor)的非递归归纳类型称为结构体(structure)或记录(record)。积类型是一种结构体,依值积(Sigma)类型也是如此。一般来说,每当我们定义一个结构体 S,我们通常会定义投影函数(projection),允许我们"析构(destruct)"每个 S 的实例并检索其字段(field)中存储的值。函数 Prod.fst 和 Prod.snd 返回一对的第一个和第二个元素,就是此类投影的例子。Recall that a non-recursive inductive type that contains only one
constructor is called a structure or record. The product type is a
structure, as is the dependent product (Sigma) type.
In general, whenever we define a structure S, we usually
define projection functions that allow us to “destruct” each
instance of S and retrieve the values that are stored in its
fields. The functions Prod.fst and Prod.snd, which return the
first and second elements of a pair, are examples of such projections.
在编写程序或形式化数学时,定义包含许多字段的结构体并不罕见。Lean 提供的 structure 命令为此过程提供了基础设施。当我们使用此命令定义结构体时,Lean 会自动生成所有的投影函数。structure 命令还允许我们基于先前定义的结构体来定义新的结构体。此外,Lean 还为定义给定结构体的实例提供了便捷的表示法。When writing programs or formalizing mathematics, it is not uncommon
to define structures containing many fields. The structure
command, available in Lean, provides infrastructure to support this
process. When we define a structure using this command, Lean
automatically generates all the projection functions. The
structure command also allows us to define new structures based on
previously defined ones. Moreover, Lean provides convenient notation
for defining instances of a given structure.
9.1. 声明结构体(Declaring Structures)9.1. Declaring Structures🔗
structure 命令本质上是一个用于定义归纳数据类型的"前端(front end)"。每个 structure 声明都会引入一个同名命名空间(namespace)。一般形式如下:The structure command is essentially a “front end” for defining
inductive data types. Every structure declaration introduces a
namespace with the same name. The general form is as follows:
structure <name> <parameters> <parent-structures> where
<constructor> :: <fields>
大多数部分是可选的。下面是一个例子:Most parts are optional. Here is an example:
structure Point (α : Type u) where
mk ::
x : α
y : α
类型 Point 的值使用 Point.mk a b 创建,点 p 的字段使用 Point.x p 和 Point.y p 访问(但 p.x 和 p.y 也可以,见下文)。structure 命令还会生成有用的递归子(recursor)和定理。以下是为上述声明生成的一些构造。Values of type Point are created using Point.mk a b, and the
fields of a point p are accessed using Point.x p and
Point.y p (but p.x and p.y also work, see below).
The structure command also generates useful recursors and
theorems. Here are some of the constructions generated for the
declaration above.
structure Point (α : Type u) where
mk ::
x : α
y : α
-- a Type
Point.{u} (α : Type u) : Type u#check PointPoint.{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。If the constructor name is not provided, then a constructor is named
mk by default.
以下是一些使用所生成构造的简单定理和表达式。通常,你可以使用 open Point 命令来避免前缀 Point。Here are some simple theorems and expressions that use the generated
constructions. As usual, you can avoid the prefix Point by using
the command open Point.
structure Point (α : Type u) where
x : α
y : α
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.x 是 Point.x p 的简写。这提供了一种访问结构体字段的便捷方式。Given p : Point Nat, the dot notation p.x is shorthand for
Point.x p. This provides a convenient way of accessing the fields
of a structure.
点表示法不仅方便访问记录的投影,也方便应用在具有相同名称的命名空间中定义的函数。回顾合取(Conjunction)一节,如果 p 的类型是 Point,表达式 p.foo 被解释为 Point.foo p,前提是 foo 的第一个非隐式参数的类型为 Point。因此,在下面的例子中,表达式 p.add q 是 Point.add p q 的简写。The dot notation is convenient not just for accessing the projections
of a record, but also for applying functions defined in a namespace
with the same name. Recall from the Conjunction section if p
has type Point, the expression p.foo is interpreted as
Point.foo p, assuming that the first non-implicit argument to
foo has type Point. The expression p.add q is therefore
shorthand for Point.add p q in the example below.
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,前提是 α 具有关联的加法运算。In the next chapter, you will learn how to define a function like
add so that it works generically for elements of Point α
rather than just Point Nat, assuming α has an associated
addition operation.
更一般地,给定表达式 p.foo x y z,其中 p : Point,Lean 会将 p 插入到 Point.foo 的第一个类型为 Point 的参数位置。例如,使用下面的标量乘法定义,p.smul 3 被解释为 Point.smul 3 p。More generally, given an expression p.foo x y z where p : Point,
Lean will insert p at the first argument to Point.foo of type
Point. For example, with the definition of scalar multiplication
below, p.smul 3 is interpreted as Point.smul 3 p.
structure Point (α : Type u) where
x : α
y : α
deriving Repr
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 函数(它将列表作为其第二个非隐式参数)采用类似的技巧是很常见的:It is common to use a similar trick with the List.map function,
which takes a list as its second non-implicit argument:
这里 xs.map f 被解释为 List.map f xs。Here xs.map f is interpreted as List.map f xs.
9.2. 对象(Objects)9.2. Objects🔗
我们一直在使用构造器来创建结构体类型的元素。对于包含许多字段的结构体来说,这通常很不方便,因为我们必须记住字段定义的顺序。因此,Lean 提供了以下替代表示法来定义结构体类型的元素。We have been using constructors to create elements of a structure
type. For structures containing many fields, this is often
inconvenient, because we have to remember the order in which the
fields were defined. Lean therefore provides the following alternative
notations for defining elements of a structure type.
{ (<field-name> := <expr>)* : structure-type }
or
{ (<field-name> := <expr>)* }
后缀 : structure-type 可以在结构体名称能从期望类型推断出时省略。例如,我们使用这种表示法来定义"点"。字段指定的顺序无关紧要,因此下面的所有表达式都定义同一个点。The suffix : structure-type can be omitted whenever the name of
the structure can be inferred from the expected type. For example, we
use this notation to define “points.” The order that the fields are
specified does not matter, so all the expressions below define the
same point.
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)。隐式字段会成为构造器的隐式参数。Fields can be marked as implicit using curly braces.
Implicit fields become implicit parameters to the constructor.
如果未指定字段的值,Lean 会尝试推断它。如果无法推断未指定的字段,Lean 会标记一个错误,指示相应的占位符无法被合成(synthesize)。If the value of a field is not specified, Lean tries to infer it. If
the unspecified fields cannot be inferred, Lean flags an error
indicating the corresponding placeholder could not be synthesized.
记录更新(record update)是另一种常见操作,即通过修改旧记录中一个或多个字段的值来创建新的记录对象。Lean 允许你通过添加注解 s with 到字段赋值之前,来指定记录的规范中未赋值的字段应从先前定义的结构体对象 s 中获取。如果提供了多个记录对象,则按顺序访问它们,直到 Lean 找到包含未指定字段的那个。如果在访问所有对象后仍有任何字段名未指定,Lean 会引发错误。Record update is another common operation which amounts to creating
a new record object by modifying the value of one or more fields in an
old one. Lean allows you to specify that unassigned fields in the
specification of a record should be taken from a previously defined
structure object s by adding the annotation s with before the field
assignments. If more than one record object is provided, then they are
visited in order until Lean finds one that contains the unspecified
field. Lean raises an error if any of the field names remain
unspecified after all the objects are visited.
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)9.3. Inheritance🔗
我们可以通过添加新字段来扩展(extend)现有结构体。这一特性使我们能够模拟一种继承(inheritance)。We can extend existing structures by adding new fields. This feature
allows us to simulate a form of inheritance.
structure Point (α : Type u) where
x : α
y : α
inductive Color where
| red | green | blue
structure ColorPoint (α : Type u) extends Point α where
c : Color
在下一个例子中,我们使用多重继承定义一个结构体,然后使用父结构体的对象来定义一个对象。In the next example, we define a structure using multiple inheritance,
and then define an object using objects of the parent structures.
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