Theorem Proving in Lean 4

2. 依值类型论(Dependent Type Theory)🔗

依值类型论(Dependent Type Theory)是一种强大且富有表现力的语言,它允许你表达复杂的数学断言、编写复杂的硬件和软件规约,并以自然且统一的方式对这两者进行推理。Lean 基于一种称为构造演算(Calculus of Constructions)的依值类型论版本,该版本具有可数的非累积宇宙(Universe)层级结构和归纳类型(Inductive Type)。到本章结束时,你将理解这其中大部分内容的含义。

2.1. 简单类型论(Simple Type Theory)🔗

“类型论(Type Theory)”因其每个表达式都有一个关联的类型(Type)而得名。例如,在给定上下文中,x + 0 可能表示一个自然数,而 f 可能表示一个关于自然数的函数。对于喜欢精确定义的人来说,Lean 中的自然数是一个任意精度的无符号整数。

以下是一些关于如何在 Lean 中声明对象并检查其类型的示例。

/- Define some constants. -/ def m : Nat := 1 -- m is a natural number def n : Nat := 0 def b1 : Bool := true -- b1 is a Boolean def b2 : Bool := false /- Check their types. -/ m : Nat#check m
m : Nat
n : Nat#check n
n : Nat
n + 0 : Nat#check n + 0
n + 0 : Nat
m * (n + 0) : Nat#check m * (n + 0)
m * (n + 0) : Nat
b1 : Bool#check b1
b1 : Bool
-- "&&" is the Boolean and b1 && b2 : Bool#check b1 && b2
b1 && b2 : Bool
-- Boolean or b1 || b2 : Bool#check b1 || b2
b1 || b2 : Bool
-- Boolean "true" Bool.true : Bool#check true
Bool.true : Bool
/- Evaluate -/ 20#eval 5 * 4
20
3#eval m + 2
3
false#eval b1 && b2
false

任何位于 /--/ 之间的文本构成一个注释块,被 Lean 忽略。类似地,两个破折号 -- 表示该行其余部分包含注释,同样被忽略。注释块可以嵌套,这使得可以“注释掉”代码块,就像在许多编程语言中一样。

def 关键字用于在工作环境中声明新的常量符号。在上面的示例中,def m : Nat := 1 定义了一个新的常量 m,其类型为 Nat,其值为 1#check 命令要求 Lean 报告这些对象的类型;在 Lean 中,用于查询系统信息的辅助命令通常以井号(#)符号开头。#eval 命令要求 Lean 计算给定的表达式。你应该尝试自己声明一些常量并类型检查一些表达式。以这种方式声明新对象是试验系统的好方法。

简单类型论(Simple Type Theory)的强大之处在于你可以从其他类型构建新类型。例如,如果 ab 是类型,则 a -> b 表示从 ab 的函数类型,而 a × b 表示由 a 的一个元素和 b 的一个元素配对而成的类型,也称为笛卡尔积(Cartesian Product)。请注意 × 是一个 Unicode 符号。合理使用 Unicode 可以提高可读性,并且所有现代编辑器都对其有很好的支持。在 Lean 标准库中,你经常看到希腊字母用来表示类型,以及 Unicode 符号 作为 -> 的更紧凑版本。

Nat Nat : Type#check Nat Nat
Nat  Nat : Type
Nat Nat : Type#check Nat -> Nat
Nat  Nat : Type
Nat × Nat : Type#check Nat × Nat
Nat × Nat : Type
Nat × Nat : Type#check Prod Nat Nat
Nat × Nat : Type
Nat Nat Nat : Type#check Nat Nat Nat
Nat  Nat  Nat : Type
Nat Nat Nat : Type#check Nat (Nat Nat)
Nat  Nat  Nat : Type
Nat × Nat Nat : Type#check Nat × Nat Nat
Nat × Nat  Nat : Type
(Nat Nat) Nat : Type#check (Nat Nat) Nat
(Nat  Nat)  Nat : Type
Nat.succ (n : Nat) : Nat#check Nat.succ
Nat.succ (n : Nat) : Nat
(0, 1) : Nat × Nat#check (0, 1)
(0, 1) : Nat × Nat
Nat.add : Nat Nat Nat#check Nat.add
Nat.add : Nat  Nat  Nat
Nat.succ 2 : Nat#check Nat.succ 2
Nat.succ 2 : Nat
Nat.add 3 : Nat Nat#check Nat.add 3
Nat.add 3 : Nat  Nat
Nat.add 5 2 : Nat#check Nat.add 5 2
Nat.add 5 2 : Nat
(5, 9).fst : Nat#check (5, 9).1
(5, 9).fst : Nat
(5, 9).snd : Nat#check (5, 9).2
(5, 9).snd : Nat
3#eval Nat.succ 2
3
7#eval Nat.add 5 2
7
5#eval (5, 9).1
5
9#eval (5, 9).2
9

再一次,你应该自己尝试一些示例。

让我们来看一些基本语法。你可以通过输入 \to\r\-> 来输入 Unicode 箭头 。你也可以使用 ASCII 替代形式 ->,因此表达式 Nat -> NatNat Nat 表示相同的意思。这两个表达式都表示将一个自然数作为输入并返回一个自然数作为输出的函数类型。笛卡尔积的 Unicode 符号 × 可以通过输入 \times 来输入。你通常会使用小写希腊字母,如 αβγ,来表示类型变量。你可以分别通过 \a\b\g 来输入这些特定字母。

这里还有几点需要注意。首先,函数 f 应用于值 x 表示为 f x(例如 Nat.succ 2)。其次,在书写类型表达式时,箭头是结合的;例如,Nat.add 的类型是 Nat Nat Nat,它等价于 Nat (Nat Nat)。因此你可以将 Nat.add 视为一个函数,它接受一个自然数并返回另一个函数,该函数再接受一个自然数并返回一个自然数。在类型论中,这通常比将 Nat.add 视为接受一对自然数作为输入并返回一个自然数作为输出的函数更方便。例如,它允许你“部分应用”函数 Nat.add。上面的示例显示 Nat.add 3 的类型是 Nat Nat,也就是说,Nat.add 3 返回一个“等待”第二个参数 n 的函数,这与直接写 Nat.add 3 n 等价。

你已经看到,如果你有 m : Natn : Nat,那么 (m, n) 表示 mn 的有序对,其类型为 Nat × Nat。这提供了一种创建自然数对的方法。反过来,如果你有 p : Nat × Nat,那么你可以写 p.1 : Natp.2 : Nat。这提供了一种提取其两个分量的方法。

2.2. 作为对象的类型(Types as objects)🔗

Lean 的依值类型论扩展简单类型论的一种方式是,类型本身——如 NatBool 这样的实体——成为了一等公民,也就是说,它们本身就是对象。为了做到这一点,它们每个也必须有一个类型。

Nat : Type#check Nat
Nat : Type
Bool : Type#check Bool
Bool : Type
Nat Bool : Type#check Nat Bool
Nat  Bool : Type
Nat × Bool : Type#check Nat × Bool
Nat × Bool : Type
Nat Nat : Type#check Nat Nat
Nat  Nat : Type
Nat × Nat Nat : Type#check Nat × Nat Nat
Nat × Nat  Nat : Type
Nat Nat Nat : Type#check Nat Nat Nat
Nat  Nat  Nat : Type
Nat Nat Nat : Type#check Nat (Nat Nat)
Nat  Nat  Nat : Type
Nat Nat Bool : Type#check Nat Nat Bool
Nat  Nat  Bool : Type
(Nat Nat) Nat : Type#check (Nat Nat) Nat
(Nat  Nat)  Nat : Type

你可以看到上面的每个表达式都是类型 Type 的对象。你也可以为类型声明新的常量:

def α : Type := Nat def β : Type := Bool def F : Type Type := List def G : Type Type Type := Prod α : Type#check α
α : Type
F α : Type#check F α
F α : Type
F Nat : Type#check F Nat
F Nat : Type
G α : Type Type#check G α
G α : Type  Type
G α β : Type#check G α β
G α β : Type
G α Nat : Type#check G α Nat
G α Nat : Type

如上例所示,你已经看到了一个类型为 Type Type Type 的函数示例,即笛卡尔积 Prod

def α : Type := Nat def β : Type := Bool α × β : Type#check Prod α β
α × β : Type
α × β : Type#check α × β
α × β : Type
Nat × Nat : Type#check Prod Nat Nat
Nat × Nat : Type
Nat × Nat : Type#check Nat × Nat
Nat × Nat : Type

这是另一个例子:给定任意类型 α,类型 List α 表示类型为 α 的元素构成的列表(List)类型。

def α : Type := Nat List α : Type#check List α
List α : Type
List Nat : Type#check List Nat
List Nat : Type

既然 Lean 中每个表达式都有一个类型,那么很自然地会问:Type 本身的类型是什么?

Type : Type 1#check Type
Type : Type 1

你实际上已经遇到了 Lean 类型系统中最微妙的方面之一。Lean 的基础框架具有一个无限的类型层级:

Type : Type 1#check Type
Type : Type 1
Type 1 : Type 2#check Type 1
Type 1 : Type 2
Type 2 : Type 3#check Type 2
Type 2 : Type 3
Type 3 : Type 4#check Type 3
Type 3 : Type 4
Type 4 : Type 5#check Type 4
Type 4 : Type 5

Type 0 视为“小”或“普通”类型的宇宙(Universe)。Type 1 是一个更大的类型宇宙,它包含 Type 0 作为一个元素,而 Type 2 是一个更大的类型宇宙,它包含 Type 1 作为一个元素。这个列表是无限的:对于每个自然数 n,都有一个 Type nTypeType 0 的缩写:

Type : Type 1#check Type
Type : Type 1
Type : Type 1#check Type 0
Type : Type 1

下表有助于具体化所讨论的关系。沿 x 轴的移动表示宇宙(Universe)的变化,而沿 y 轴的移动表示有时被称为“度(Degree)”的变化。

sort(种类)

PropSort 0

TypeSort 1

Type 1Sort 2

Type 2Sort 3

...

type(类型)

True

Bool

Nat -> Type

Type -> Type 1

...

term(项)

True.intro

true

fun n => Fin n

fun (_ : Type) => Type

...

然而,某些操作需要跨越类型宇宙(Universe)是多态的(Polymorphic)。例如,List α 对于任何类型 α 都应有意义,无论 α 位于哪个类型宇宙中。这解释了函数 List 的类型签名:

List.{u} (α : Type u) : Type u#check List
List.{u} (α : Type u) : Type u

这里 u 是一个取值范围覆盖类型层级的变量。#check 命令的输出意味着每当 α 具有类型 Type u 时,List α 也具有类型 Type u。函数 Prod 同样是多态的:

Prod.{u, v} (α : Type u) (β : Type v) : Type (max u v)#check Prod
Prod.{u, v} (α : Type u) (β : Type v) : Type (max u v)

为了定义多态常量,Lean 允许你使用 universe 命令显式声明宇宙(Universe)变量:

universe u def F (α : Type u) : Type u := Prod α α F.{u} (α : Type u) : Type u#check F
F.{u} (α : Type u) : Type u

你可以通过定义 F 时提供宇宙参数来避免使用 universe 命令:

def F.{u} (α : Type u) : Type u := Prod α α F.{u} (α : Type u) : Type u#check F
F.{u} (α : Type u) : Type u

2.3. 函数抽象与求值(Function Abstraction and Evaluation)🔗

Lean 提供了一个 fun(或 λ)关键字,用于从表达式创建函数,如下所示:

fun x => x + 5 : Nat Nat#check fun (x : Nat) => x + 5
fun x => x + 5 : Nat  Nat
-- λ and fun mean the same thing fun x => x + 5 : Nat Nat#check λ (x : Nat) => x + 5
fun x => x + 5 : Nat  Nat

在这个示例中,类型 Nat 可以被推断出来:

fun x => x + 5 : Nat Nat#check fun x => x + 5
fun x => x + 5 : Nat  Nat
fun x => x + 5 : Nat Nat#check λ x => x + 5
fun x => x + 5 : Nat  Nat

你可以通过传递所需的参数来求值一个 lambda(λ)函数:

15#eval (λ x : Nat => x + 5) 10
15

从另一个表达式创建函数的过程称为λ 抽象(Lambda Abstraction)。假设你有一个变量 x : α 并且你可以构造一个表达式 t : β,那么表达式 fun (x : α) => t,或等价地 λ (x : α) => t,是一个类型为 α β 的对象。将其视为从 αβ 的函数,它将任意值 x 映射到值 t

这里还有一些例子

fun x y => if (!y) = true then x + 1 else x + 2 : Nat Bool Nat#check fun x : Nat => fun y : Bool => if not y then x + 1 else x + 2
fun x y => if (!y) = true then x + 1 else x + 2 : Nat  Bool  Nat
fun x y => if (!y) = true then x + 1 else x + 2 : Nat Bool Nat#check fun (x : Nat) (y : Bool) => if not y then x + 1 else x + 2
fun x y => if (!y) = true then x + 1 else x + 2 : Nat  Bool  Nat
fun x y => if (!y) = true then x + 1 else x + 2 : Nat Bool Nat#check fun x y => if not y then x + 1 else x + 2
fun x y => if (!y) = true then x + 1 else x + 2 : Nat  Bool  Nat

Lean 将最后三个示例解释为相同的表达式;在最后一个表达式中,Lean 从表达式 if not y then x + 1 else x + 2 推断出 xy 的类型。

一些数学上常见的函数操作示例可以用λ 抽象(Lambda Abstraction)来描述:

def f (n : Nat) : String := toString n def g (s : String) : Bool := s.length > 0 fun x => x : Nat Nat#check fun x : Nat => x
fun x => x : Nat  Nat
fun x => true : Nat Bool#check fun unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x : Nat => true
fun x => true : Nat  Bool
fun x => g (f x) : Nat Bool#check fun x : Nat => g (f x)
fun x => g (f x) : Nat  Bool
fun x => g (f x) : Nat Bool#check fun x => g (f x)
fun x => g (f x) : Nat  Bool

思考这些表达式的含义。表达式 fun x : Nat => x 表示 Nat 上的恒等函数(Identity Function),表达式 fun x : Nat => true 表示始终返回 true 的常函数(Constant Function),而 fun x : Nat => g (f x) 表示 fg 的复合(Composition)。通常,你可以省略类型标注,让 Lean 为你推断。因此,例如,你可以写 fun x => g (f x) 来代替 fun x : Nat => g (f x)

你可以将函数作为参数传递,并通过为它们命名 fg,然后在实现中使用这些函数:

fun g f x => g (f x) : (String Bool) (Nat String) Nat Bool#check fun (g : String Bool) (f : Nat String) (x : Nat) => g (f x)
fun g f x => g (f x) : (String  Bool)  (Nat  String)  Nat  Bool

你也可以将类型作为参数传递:

fun α β γ g f x => g (f x) : (α β γ : Type) (β γ) (α β) α γ#check fun (α β γ : Type) (g : β γ) (f : α β) (x : α) => g (f x)
fun α β γ g f x => g (f x) : (α β γ : Type)  (β  γ)  (α  β)  α  γ

例如,最后一个表达式表示一个函数,它接受三个类型 αβγ,以及两个函数 g : β γf : α β,并返回 gf 的复合。(理解这个函数的类型需要理解依值积(Dependent Product),这将在下面解释。)

lambda(λ)表达式的一般形式是 fun (x : α) => t,其中变量 x 是一个“绑定变量(Bound Variable)”:它实际上是一个占位符,其“作用域”不超出表达式 t。例如,表达式 fun (b : β) (x : α) => b 中的变量 b 与之前声明的常量 b 毫无关系。事实上,该表达式表示的函数与 fun (u : β) (z : α) => u 相同。

形式上,在绑定变量重命名下相同的表达式称为α 等价(Alpha Equivalent),并且被认为“相同”。Lean 能够识别这种等价性。

注意,将项 t : α β 应用于项 s : α 会得到表达式 t s : β。回到前面的例子并为了清晰而重命名绑定变量,注意以下表达式的类型:

(fun x => x) 1 : Nat#check (fun x : Nat => x) 1
(fun x => x) 1 : Nat
(fun x => true) 1 : Bool#check (fun unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x : Nat => true) 1
(fun x => true) 1 : Bool
def f (n : Nat) : String := toString n def g (s : String) : Bool := s.length > 0 (fun α β γ u v x => u (v x)) Nat String Bool g f 0 : Bool#check (fun (α β γ : Type) (u : β γ) (v : α β) (x : α) => u (v x)) Nat String Bool g f 0
(fun α β γ u v x => u (v x)) Nat String Bool g f 0 : Bool

正如预期的那样,表达式 (fun x : Nat => x) 1 的类型是 Nat。事实上,更精确地说:将表达式 (fun x : Nat => x) 应用于 1 应该“返回”值 1。而事实上,确实如此:

1#eval (fun x : Nat => x) 1
1
true#eval (fun unused variable `x` Note: This linter can be disabled with `set_option linter.unusedVariables false`x : Nat => true) 1
true

稍后你将看到这些项是如何求值的。现在,只需注意这是依值类型论的一个重要特征:每个项都具有计算行为,并支持规范化(Normalization)的概念。原则上,归约为相同值的两个项称为定义上相等(Definitionally Equal)。它们被 Lean 的类型检查器视为“相同”,并且 Lean 会尽力识别和支持这种等同。

Lean 是一门完整的编程语言。它具有生成二进制可执行文件的编译器和交互式解释器。你可以使用 #eval 命令来执行表达式,这是测试函数的推荐方式。

2.4. 定义(Definitions)🔗

回顾一下,def 关键字提供了一种声明新的命名对象的重要方式。

def double (x : Nat) : Nat := x + x

如果你了解函数在其他编程语言中的工作方式,这看起来可能会更熟悉。名称 double 被定义为一个函数,它接受一个类型为 Nat 的输入参数 x,调用的结果是 x + x,因此返回类型是 Nat。然后你可以使用以下方式调用这个函数:

6#eval double 3
6

在这种情况下,你可以将 def 视为一种命名的 fun。以下产生相同的结果:

def double : Nat Nat := fun x => x + x 6#eval double 3
6

当 Lean 有足够的信息时,你可以省略类型声明。类型推断(Type Inference)是 Lean 的重要组成部分:

def double := fun (x : Nat) => x + x

定义的一般形式是 def foo : α := bar,其中 α 是表达式 bar 返回的类型。Lean 通常可以推断出类型 α,但显式写出它通常是一个好主意。这明确了你的意图,并且如果定义的右侧没有匹配的类型,Lean 将标记错误。

右侧 bar 可以是任何表达式,而不仅仅是 lambda。因此 def 也可以用于简单地命名一个值,就像这样:

def pi := 3.141592654

def 可以接受多个输入参数。让我们创建一个将两个自然数相加的函数:

def add (x y : Nat) := x + y 5#eval add 3 2
5

参数列表可以这样分隔:

def add (x : Nat) (y : Nat) := x + y 22#eval add (double 3) (7 + 9)
22

注意,这里我们调用了 double 函数来创建传递给 add 的第一个参数。

你可以在 def 内部使用其他更有趣的表达式:

def greater (x y : Nat) := if x > y then x else y

你可能能猜到这将做什么。

你也可以定义一个将另一个函数作为输入的函数。以下函数将给定的函数调用两次,将第一次调用的输出传递给第二次:

def doTwice (f : Nat Nat) (x : Nat) : Nat := f (f x) 8#eval doTwice double 2
8

现在,为了更抽象一些,你也可以指定像类型参数这样的参数:

def compose (α β γ : Type) (g : β γ) (f : α β) (x : α) : γ := g (f x)

这意味着 compose 是一个接受任意两个函数作为输入参数的函数,只要这些函数各自只接受一个输入。类型代数 β γα β 意味着第二个函数的输出类型必须与第一个函数的输入类型匹配——这很合理,否则这两个函数将不可复合。

compose 还接受一个类型为 α 的第三个参数,它使用这个参数来调用第二个函数(局部命名为 f),并将该函数的结果(类型为 β)作为输入传递给第一个函数(局部命名为 g)。第一个函数返回类型 γ,因此这也是 compose 函数的返回类型。

compose 也非常通用,它可以处理任何类型 α β γ。这意味着只要两个函数各自接受一个参数,并且第二个函数的输出类型与第一个函数的输入类型匹配,compose 就可以复合几乎任何两个函数。例如:

def square (x : Nat) : Nat := x * x 18#eval compose Nat Nat Nat double square 3
18

2.5. 局部定义(Local Definitions)🔗

Lean 还允许你使用 let 关键字引入“局部”定义。表达式 let a := t1; t2 在定义上等价于将 t2 中的每个 a 替换为 t1 后得到的结果。

let y := 2 + 2; y * y : Nat#check let y := 2 + 2; y * y
let y := 2 + 2;
y * y : Nat
16#eval let y := 2 + 2; y * y
16
def twice_double (x : Nat) : Nat := let y := x + x; y * y 16#eval twice_double 2
16

这里,twice_double x 在定义上等价于项 (x + x) * (x + x)

你可以通过链式使用 let 语句来组合多个赋值:

let y := 2 + 2; let z := y + y; z * z : Nat#check let y := 2 + 2; let z := y + y; z * z
let y := 2 + 2;
let z := y + y;
z * z : Nat
64#eval let y := 2 + 2; let z := y + y; z * z
64

当使用换行时,; 可以省略。

def t (x : Nat) : Nat := let y := x + x y * y

注意,表达式 let a := t1; t2 的含义与 (fun a => t2) t1 的含义非常相似,但两者并不相同。在第一个表达式中,你应该将 t2a 的每个出现视为 t1 的语法缩写。在第二个表达式中,a 是一个变量,表达式 fun a => t2 必须独立于 a 的值而具有意义。let 结构是一种更强的缩写方式,并且存在某些形式为 let a := t1; t2 的表达式无法表示为 (fun a => t2) t1。作为练习,试着理解为什么下面的 foo 定义可以通过类型检查,而 bar 的定义却不能。

def foo := let a := Nat; fun x : a => x + 2 /- def bar := (fun a => fun x : a => x + 2) Nat -/

2.6. 变量与段(Variables and Sections)🔗

考虑以下三个函数定义:

def compose (α β γ : Type) (g : β γ) (f : α β) (x : α) : γ := g (f x) def doTwice (α : Type) (h : α α) (x : α) : α := h (h x) def doThrice (α : Type) (h : α α) (x : α) : α := h (h (h x))

Lean 为你提供了 variable 命令,使这样的声明看起来更紧凑:

variable (α β γ : Type) def compose (g : β γ) (f : α β) (x : α) : γ := g (f x) def doTwice (h : α α) (x : α) : α := h (h x) def doThrice (h : α α) (x : α) : α := h (h (h x))

你可以声明任何类型的变量,而不仅仅是 Type 本身:

variable (α β γ : Type) variable (g : β γ) (f : α β) (h : α α) variable (x : α) def compose := g (f x) def doTwice := h (h x) def doThrice := h (h (h x)) def compose : (α β γ : Type) (β γ) (α β) α γ := fun α β γ g f x => g (f x)#print compose
def compose : (α β γ : Type)  (β  γ)  (α  β)  α  γ :=
fun α β γ g f x => g (f x)
def doTwice : (α : Type) (α α) α α := fun α h x => h (h x)#print doTwice
def doTwice : (α : Type)  (α  α)  α  α :=
fun α h x => h (h x)
def doThrice : (α : Type) (α α) α α := fun α h x => h (h (h x))#print doThrice
def doThrice : (α : Type)  (α  α)  α  α :=
fun α h x => h (h (h x))

将它们打印出来显示,所有三组定义具有完全相同的效果。

variable 命令指示 Lean 将声明的变量作为绑定变量插入到引用它们的定义中。Lean 足够智能,能够找出哪些变量在定义中被显式或隐式地使用。因此,你在编写定义时可以将 αβγgfhx 视为固定对象,然后让 Lean 自动为你抽象定义。

以这种方式声明时,变量将保持有效直到当前文件末尾。然而,有时限制变量的作用域是有用的。为此,Lean 提供了 section 的概念:

section useful variable (α β γ : Type) variable (g : β γ) (f : α β) (h : α α) variable (x : α) def compose := g (f x) def doTwice := h (h x) def doThrice := h (h (h x)) end useful

当段(Section)关闭时,变量将超出作用域,无法再被引用。

你不必缩进段内的行。也不必为段命名,也就是说,你可以使用匿名的 section / end 对。但是,如果你确实命名了一个段,则必须使用相同的名称来关闭它。段也可以嵌套,这允许你逐步声明新的变量。

2.7. 命名空间(Namespaces)🔗

Lean 为你提供了将定义分组到嵌套的、层次化的命名空间(Namespace)中的能力:

namespace Foo def a : Nat := 5 def f (x : Nat) : Nat := x + 7 def fa : Nat := f a def ffa : Nat := f (f a) Foo.a : Nat#check a
Foo.a : Nat
Foo.f (x : Nat) : Nat#check f
Foo.f (x : Nat) : Nat
Foo.fa : Nat#check fa
Foo.fa : Nat
Foo.ffa : Nat#check ffa
Foo.ffa : Nat
Foo.fa : Nat#check Foo.fa
Foo.fa : Nat
end Foo -- #check a -- error -- #check f -- error Foo.a : Nat#check Foo.a
Foo.a : Nat
Foo.f (x : Nat) : Nat#check Foo.f
Foo.f (x : Nat) : Nat
Foo.fa : Nat#check Foo.fa
Foo.fa : Nat
Foo.ffa : Nat#check Foo.ffa
Foo.ffa : Nat
open Foo Foo.a : Nat#check a
Foo.a : Nat
Foo.f (x : Nat) : Nat#check f
Foo.f (x : Nat) : Nat
Foo.fa : Nat#check fa
Foo.fa : Nat
Foo.fa : Nat#check Foo.fa
Foo.fa : Nat

当你声明在命名空间 Foo 中工作时,你声明的每个标识符都有一个以 “Foo.” 为前缀的全名。在命名空间内部,你可以使用较短的名字来引用标识符,但一旦结束命名空间,你就必须使用较长的名字。与 section 不同,命名空间需要名称。在根级别只有一个匿名命名空间。

open 命令将较短的名字引入当前上下文。通常,当你导入一个模块时,你会希望打开它所包含的一个或多个命名空间,以便使用较短的标识符。但有时你希望将这些信息保留在完全限定名称的保护下,例如当它们与你想使用的另一个命名空间中的标识符冲突时。因此,命名空间为你提供了一种管理工作环境中名称的方法。

例如,Lean 将涉及列表(List)的定义和定理分组到命名空间 List 中。

List.nil.{u} {α : Type u} : List α#check List.nil
List.nil.{u} {α : Type u} : List α
List.cons.{u} {α : Type u} (head : α) (tail : List α) : List α#check List.cons
List.cons.{u} {α : Type u} (head : α) (tail : List α) : List α
List.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α β) (l : List α) : List β#check List.map
List.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α  β) (l : List α) : List β

命令 open List 允许你使用较短的名称:

open List List.nil.{u} {α : Type u} : List α#check nil
List.nil.{u} {α : Type u} : List α
List.cons.{u} {α : Type u} (head : α) (tail : List α) : List α#check cons
List.cons.{u} {α : Type u} (head : α) (tail : List α) : List α
List.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α β) (l : List α) : List β#check map
List.map.{u_1, u_2} {α : Type u_1} {β : Type u_2} (f : α  β) (l : List α) : List β

与段(Section)类似,命名空间可以嵌套:

namespace Foo def a : Nat := 5 def f (x : Nat) : Nat := x + 7 def fa : Nat := f a namespace Bar def ffa : Nat := f (f a) Foo.fa : Nat#check fa
Foo.fa : Nat
Foo.Bar.ffa : Nat#check ffa
Foo.Bar.ffa : Nat
end Bar Foo.fa : Nat#check fa
Foo.fa : Nat
Foo.Bar.ffa : Nat#check Bar.ffa
Foo.Bar.ffa : Nat
end Foo Foo.fa : Nat#check Foo.fa
Foo.fa : Nat
Foo.Bar.ffa : Nat#check Foo.Bar.ffa
Foo.Bar.ffa : Nat
open Foo Foo.fa : Nat#check fa
Foo.fa : Nat
Foo.Bar.ffa : Nat#check Bar.ffa
Foo.Bar.ffa : Nat

已关闭的命名空间可以在之后重新打开,甚至在另一个文件中也可以:

namespace Foo def a : Nat := 5 def f (x : Nat) : Nat := x + 7 def fa : Nat := f a end Foo Foo.a : Nat#check Foo.a
Foo.a : Nat
Foo.f (x : Nat) : Nat#check Foo.f
Foo.f (x : Nat) : Nat
namespace Foo def ffa : Nat := f (f a) end Foo

与段(Section)一样,嵌套的命名空间必须按照打开的顺序关闭。命名空间和段服务于不同的目的:命名空间组织数据,而段声明变量以插入到定义中。段也适用于界定诸如 set_optionopen 等命令的作用域。

然而,在许多方面,namespace ... end 块的行为与 section ... end 块相同。特别地,如果你在命名空间内使用 variable 命令,其作用域仅限于该命名空间。类似地,如果你在命名空间内使用 open 命令,其效果在命名空间关闭时消失。

2.8. 什么使依值类型论成为依值的?(What makes dependent type theory dependent?)🔗

简短的解释是:类型可以依赖于参数。你已经看到了一个好例子:类型 List α 依赖于参数 α,而这种依赖性正是区分 List NatList Bool 的原因。另一个例子,考虑类型 Vector α n,它是长度为 nα 类型元素的向量(Vector)类型。这个类型依赖于两个参数:向量中元素的类型(α : Type)和向量的长度 n : Nat

假设你想写一个函数 cons,它在列表的头部插入一个新元素。cons 应该有什么类型呢?这样的函数是多态的(Polymorphic):你期望 NatBool 或任意类型 αcons 函数行为方式相同。因此,将类型作为 cons 的第一个参数是有意义的,这样对于任何类型 αcons α 就是针对 α 类型列表的插入函数。换句话说,对于每个 αcons α 是一个接受元素 a : α 和列表 as : List α 并返回一个新列表的函数,因此你有 cons α a as : List α

显然,cons α 的类型应该是 α List α List α。但是 cons 本身应该有什么类型呢?最初猜测可能是 Type α List α List α,但仔细想想,这说不通:这个表达式中的 α 不指向任何东西,而它应该指向类型为 Type 的参数。换句话说,假设 α : Type 是函数的第一个参数,那么接下来两个元素的类型是 αList α。这些类型根据第一个参数 α 而变化。

def cons (α : Type) (a : α) (as : List α) : List α := List.cons a as cons Nat : Nat List Nat List Nat#check cons Nat
cons Nat : Nat  List Nat  List Nat
cons Bool : Bool List Bool List Bool#check cons Bool
cons Bool : Bool  List Bool  List Bool
cons (α : Type) (a : α) (as : List α) : List α#check cons
cons (α : Type) (a : α) (as : List α) : List α

这就是一个依值函数类型(Dependent Function Type),或称依值箭头类型的实例。给定 α : Typeβ : α Type,将 β 视为 α 上的一个类型族(Family of Types),也就是说,对于每个 a : α,都有一个类型 β a。在这种情况下,类型 (a : α) β a 表示函数 f 的类型,其性质是对于每个 a : αf aβ a 的一个元素。换句话说,f 返回的值的类型取决于其输入。

注意,(a : α) β 对于任何表达式 β : Type 都是有意义的。当 β 的值依赖于 a 时(例如上一段中的表达式 β a),(a : α) β 表示一个依值函数类型。当 β 不依赖于 a 时,(a : α) β 与类型 α β 没有区别。事实上,在依值类型论(以及 Lean)中,α β 只是当 β 不依赖于 a(a : α) β 的记法。

回到列表的例子,你可以使用 #check 命令检查以下 List 函数的类型。@ 符号以及圆括号和花括号之间的区别将在稍后解释。

@List.cons : {α : Type u_1} α List α List α#check @List.cons
@List.cons : {α : Type u_1}  α  List α  List α
@List.nil : {α : Type u_1} List α#check @List.nil
@List.nil : {α : Type u_1}  List α
@List.length : {α : Type u_1} List α Nat#check @List.length
@List.length : {α : Type u_1}  List α  Nat
@List.append : {α : Type u_1} List α List α List α#check @List.append
@List.append : {α : Type u_1}  List α  List α  List α

正如依值函数类型 (a : α) β a 通过允许 β 依赖于 a 来推广函数类型 α β 的概念一样,依值笛卡尔积类型 (a : α) × β a 以同样的方式推广笛卡尔积 α × β。依值积(Dependent Product)也称为sigma(Σ)类型,你也可以将其写为 Σ a : α, β a。你可以使用 a, bSigma.mk a b 来创建一个依值对(Dependent Pair)。 字符可以分别通过输入 \langle\rangle\<\> 来输入。

universe u v def f (α : Type u) (β : α Type v) (a : α) (b : β a) : (a : α) × β a := a, b def g (α : Type u) (β : α Type v) (a : α) (b : β a) : Σ a : α, β a := Sigma.mk a b def h1 (x : Nat) : Nat := (f Type (fun α => α) Nat x).2 5#eval h1 5
5
def h2 (x : Nat) : Nat := (g Type (fun α => α) Nat x).2 5#eval h2 5
5

上面的函数 fg 表示相同的函数。

2.9. 隐式参数(Implicit Arguments)🔗

假设我们有一个如下的列表实现:

Lst.{u} (α : Type u) : Type u#check Lst
Lst.{u} (α : Type u) : Type u
Lst.cons.{u} (α : Type u) (a : α) (as : Lst α) : Lst α#check Lst.cons
Lst.cons.{u} (α : Type u) (a : α) (as : Lst α) : Lst α
Lst.nil.{u} (α : Type u) : Lst α#check Lst.nil
Lst.nil.{u} (α : Type u) : Lst α
Lst.append.{u} (α : Type u) (as bs : Lst α) : Lst α#check Lst.append
Lst.append.{u} (α : Type u) (as bs : Lst α) : Lst α

然后,你可以按如下方式构造 Nat 的列表:

Lst.cons Nat 0 (Lst.nil Nat) : Lst Nat#check Lst.cons Nat 0 (Lst.nil Nat)
Lst.cons Nat 0 (Lst.nil Nat) : Lst Nat
def as : Lst Nat := Lst.nil Nat def bs : Lst Nat := Lst.cons Nat 5 (Lst.nil Nat) Lst.append Nat as bs : Lst Nat#check Lst.append Nat as bs
Lst.append Nat as bs : Lst Nat

由于构造器(Constructor)在类型上是多态的,我们必须反复插入类型 Nat 作为参数。但这个信息是冗余的:我们可以从 Lst.cons Nat 5 (Lst.nil Nat) 中推断出参数 α,因为第二个参数 5 的类型是 Nat。类似地,我们可以在 Lst.nil Nat 中推断出参数,不是从该表达式中的其他任何东西,而是从它作为参数传递给函数 Lst.cons 这一事实,而 Lst.cons 在该位置期望一个类型为 Lst α 的元素。

这是依值类型论的一个核心特征:项携带大量信息,并且通常其中一些信息可以从上下文中推断出来。在 Lean 中,使用下划线 _ 来指定系统应自动填入该信息。这被称为“隐式参数(Implicit Argument)”。

Lst.cons Nat 0 (Lst.nil Nat) : Lst Nat#check Lst.cons _ 0 (Lst.nil _)
Lst.cons Nat 0 (Lst.nil Nat) : Lst Nat
def as : Lst Nat := Lst.nil _ def bs : Lst Nat := Lst.cons _ 5 (Lst.nil _) Lst.append Nat as bs : Lst Nat#check Lst.append _ as bs
Lst.append Nat as bs : Lst Nat

然而,输入所有这些下划线仍然很繁琐。当一个函数接受一个通常可以从上下文中推断出来的参数时,Lean 允许你指定该参数默认保持为隐式的。这通过将参数放在花括号中来实现,如下所示:

universe u def Lst (α : Type u) : Type u := List α def Lst.cons {α : Type u} (a : α) (as : Lst α) : Lst α := List.cons a as def Lst.nil {α : Type u} : Lst α := List.nil def Lst.append {α : Type u} (as bs : Lst α) : Lst α := List.append as bs Lst.cons 0 Lst.nil : Lst Nat#check Lst.cons 0 Lst.nil
Lst.cons 0 Lst.nil : Lst Nat
def as : Lst Nat := Lst.nil def bs : Lst Nat := Lst.cons 5 Lst.nil as.append bs : Lst Nat#check Lst.append as bs
as.append bs : Lst Nat

唯一的变化是在变量声明中 α : Type u 周围加上了花括号。我们也可以在函数定义中使用这种手段:

universe u def ident {α : Type u} (x : α) := x

检查 ident 的类型需要将其括在括号中以避免显示其签名:

ident : ?m.1 ?m.1#check (ident)
ident : ?m.1  ?m.1
ident 1 : Nat#check ident 1
ident 1 : Nat
ident "hello" : String#check ident "hello"
ident "hello" : String
@ident : {α : Type u_1} α α#check @ident
@ident : {α : Type u_1}  α  α

这使得 ident 的第一个参数变为隐式。在记法上,这隐藏了类型的指定,使得 ident 看起来只接受一个任意类型的参数。事实上,标准库中的函数 id 正是以这种方式定义的。我们在这里选择了一个非传统的名称,仅仅是为了避免名称冲突。

当使用 variable 命令声明变量时,也可以将其指定为隐式:

universe u section variable {α : Type u} variable (x : α) def ident := x end ident.{u} {α : Type u} (x : α) : α#check ident
ident.{u} {α : Type u} (x : α) : α
ident 4 : Nat#check ident 4
ident 4 : Nat
ident "hello" : String#check ident "hello"
ident "hello" : String

这里的 ident 定义与上面的定义具有相同的效果。

Lean 具有非常复杂的机制来实例化隐式参数,我们将看到它们可以用来推断函数类型、谓词(Predicate)甚至证明(Proof)。在项中实例化这些“洞(Hole)”或“占位符(Placeholder)”的过程通常称为精化(Elaboration)。隐式参数的存在意味着有时可能没有足够的信息来精确确定一个表达式的含义。像 idList.nil 这样的表达式被称为多态的(Polymorphic),因为它在不同上下文中可以具有不同含义。

你可以通过写 (e : T) 来指定表达式 e 的类型 T。这会指示 Lean 的精化器(Elaborator)在尝试解析隐式参数时使用 T 作为 e 的类型。在下面的第二对示例中,这种机制用于指定表达式 idList.nil 的所需类型:

[] : List ?m.1#check (List.nil)
[] : List ?m.1
id : ?m.1 ?m.1#check (id)
id : ?m.1  ?m.1
[] : List Nat#check (List.nil : List Nat)
[] : List Nat
id : Nat Nat#check (id : Nat Nat)
id : Nat  Nat

数字字面量(Numeral)在 Lean 中是重载的,但当无法推断数字字面量的类型时,Lean 默认假定它是一个自然数。因此,下面前两个 #check 命令中的表达式以相同方式精化,而第三个 #check 命令将 2 解释为整数。

2 : Nat#check 2
2 : Nat
2 : Nat#check (2 : Nat)
2 : Nat
2 : Int#check (2 : Int)
2 : Int

然而,有时我们可能会遇到这样的情况:我们已将函数的某个参数声明为隐式,但现在想要显式提供该参数。如果 foo 是这样一个函数,那么记法 @foo 表示将所有参数都变为显式的同一函数。

@id : {α : Sort u_1} α α#check @id
@id : {α : Sort u_1}  α  α
id : Nat Nat#check @id Nat
id : Nat  Nat
id : Bool Bool#check @id Bool
id : Bool  Bool
id 1 : Nat#check @id Nat 1
id 1 : Nat
id true : Bool#check @id Bool true
id true : Bool

注意,现在第一个 #check 命令给出了标识符 id 的类型,没有插入任何占位符。此外,输出表明第一个参数是隐式的。