2.依赖型理论
依赖类型理论是一种强大而富有表现力的语言,允许我们表达复杂的数学断言,编写复杂的硬件和软件规范,并以自然和统一的方式对这两者进行推理。 精益是基于一种称为归纳建构微积分的依赖型理论,具有可累积的非累积宇宙和归纳类型的层次结构。 到本章结束时,您将了解这意味着什么。
2.1 简单类型理论
作为数学的基础,集合论有一个相当吸引人的简单本体论。 一切都是一组,包括数字,函数,三角形,随机过程和黎曼流形。 一个值得注意的事实是,人们可以从少数描述一些基本集合理论结构的公理构建一个丰富的数学世界。
但是出于许多目的,包括正式定理证明,最好有一个基础设施来帮助我们管理和跟踪我们正在使用的各种数学对象。 “类型理论”的名称源于每个表达式都具有关联类型的事实。 例如,在给定的上下文中,x + 0可以表示自然数,f可以表示对自然数的函数。
以下是我们如何在Lean中声明对象并检查其类型的一些示例。
import standard
open bool nat
/- declare some constants -/
constant m : nat -- m is a natural number
constant n : nat
constants b1 b2 : bool -- declare two constants at once
/- check their types -/
check m -- output: nat
check n
check n + 0 -- nat
check m * (n + 0) -- nat
check b1 -- bool
check b1 && b2 -- "&&" is boolean and
check b1 || b2 -- boolean or
check tt -- boolean "true"
第一个命令,import standard,告诉Lean我们打算使用标准库。下一个命令open bool nat告诉Lean我们将使用来自布尔理论和自然数理论的常数,事实和符号。在技术方面,bool和nat是命名空间;稍后您将了解更多相关信息。为了缩短示例,我们通常会隐藏相关导入,如果在前面的示例中已经导入。
/ - 和 - /注释表明下一行是精益忽略的注释块。类似地,两个破折号表示该行的其余部分包含也被忽略的注释。注释块可以嵌套,从而可以“注释掉”代码块,就像在许多编程语言中一样。
constant和constants命令将新的常量符号引入工作环境,check命令要求Lean报告其类型。你应该测试一下,然后尝试输入你自己的一些例子。以这种方式声明新对象是试验系统的好方法,但最终不可取:精益是一个基础系统,也就是说,它为我们提供了强大的机制来定义我们需要的所有数学对象,而不是简单地将它们假装到系统中。我们将在后面的章节中探讨这些机制。
简单类型理论的强大之处在于可以构建其他类型的新类型。例如,如果A和B是类型,则A→B表示从A到B的函数类型,A×B表示笛卡尔积,即,由A元素和B元素组成的有序对的类型。
open prod -- makes notation for the product available
constants m n : nat
constant f : nat → nat -- type the arrow as "\to" or "\r"
constant f' : nat -> nat -- alternative ASCII notation
constant f'' : ℕ → ℕ -- \nat is alternative notation for nat
constant p : nat × nat -- type the product as "\times"
constant q : prod nat nat -- alternative notation
constant g : nat → nat → nat
constant g' : nat → (nat → nat) -- has the same type as g!
constant h : nat × nat → nat
constant F : (nat → nat) → nat -- a "functional"
check f -- ℕ → ℕ
check f n -- ℕ
check g m n -- ℕ
check g m -- ℕ → ℕ
check pair m n -- ℕ × ℕ
check pr1 p -- ℕ
check pr2 p -- ℕ
check pr1 (pair m n) -- ℕ
check pair (pr1 p) n -- ℕ × ℕ
check F f -- ℕ
符号ℕ是nat的符号;您可以输入\ nat输入它。还有一些事情需要注意。首先,将函数f应用于值x表示为f x。其次,在写类型表达式时,箭头与右侧相关联;例如,g的类型是nat→(nat→nat)。因此,我们可以将g视为一个函数,它接受自然数并返回另一个采用自然数并返回自然数的函数。在类型理论中,这通常比将g作为以一对自然数作为输入的函数编写更方便,并返回自然数作为输出。例如,它允许我们“部分应用”函数g。上面的例子表明g m的类型为nat→nat,即“等待”第二个参数n的函数,然后返回g m n。采用nat×nat→nat类型的函数h和“重新定义”它看起来像g是一个称为currying的过程,我们将回到下面。
到目前为止,您可能还猜到,在精益中,对m n表示有序的m和n对,如果p是一对,则pr1 p和pr2 p表示两个投影。
2.2 作为对象的类型
精益依赖类型理论扩展简单类型理论的一种方式是类型本身 - 像nat和bool这样的实体 - 是一等公民,也就是说它们本身就是研究对象。 对于那种情况,他们每个人也必须有一个类型。
check nat -- Type₁
check bool -- Type₁
check nat → bool -- Type₁
check nat × bool -- Type₁
check nat → nat -- ...
check nat × nat → nat
check nat → nat → nat
check nat → (nat → nat)
check nat → nat → bool
check (nat → nat) → nat
我们看到上面的每个表达式都是Type 1类型的对象。 我们马上解释下标1。 我们还可以为类型声明新的常量和构造函数:
constants A B : Type
constant F : Type → Type
constant G : Type → Type → Type
check A -- Type
check F A -- Type
check F nat -- Type
check G A -- Type → Type
check G A B -- Type
check G A nat -- Type
实际上,我们已经看到类型→类型→类型的函数的示例,即笛卡尔积。
constants A B : Type
check prod -- Type → Type → Type
check prod A -- Type → Type
check prod A B -- Type
check prod nat nat -- Type₁
这是另一个例子:给定任何类型A,类型列表A表示类型A的元素列表的类型。
import data.list
open list
constant A : Type
check list -- Type → Type
check list A -- Type
check list nat -- Type₁
我们将看到将类型构造函数视为普通数学函数实例的能力是依赖类型理论的强大特征。
对于那些对集合理论基础更为熟悉的人来说,将类型视为集合可能会有所帮助,在这种情况下,类型的元素只是集合的元素。 但附近有潜伏的圆形。 类型本身是一个像nat的表达式; 如果nat有一个类型,那么Type不应该有类型吗?
check Type -- Type
精益的输出似乎表明Type是其自身的一个元素。 但这是误导。 拉塞尔的悖论表明,它与集合论的其他公理不一致,假设存在一组所有集合,并且可以在依赖型理论中得出类似的悖论。 那么,精益不一致吗?
究竟是什么,精益的基础片段实际上有一个类型的层次结构,
Type.{1} : Type.{2} : Type.{3} : ....
将Type。{1}视为“小”或“普通”类型的宇宙。 类型。{2}是一个更大的类型世界,它包含Type。{1}作为元素。 当我们声明一个常量A:Type时,Lean隐式创建一个变量u,并声明A:Type。{u}。 换句话说,A是某些未指定的Universe中的类型。 表达式A则是多态的; 无论什么时候出现,精益都会默默地试图推断出A所居住的宇宙,并保持尽可能多的普遍性。
您可以让Lean的漂亮打印机明确显示此信息,并使用其他注释明确指定Universe级别。
constants A B : Type
check A -- A : Type
check B -- B : Type
check Type -- Type : Type
check Type → Type -- Type → Type : Type
set_option pp.universes true -- display universe information
check A -- A.{l_1} : Type.{l_1}
check B -- B.{l_1} : Type.{l_1}
check Type -- Type.{l_1} : Type.{l_1 + 1}
check Type → Type -- Type.{l_1} → Type.{l_2} : Type.{imax (l_1+1) (l_2+1)}
universe u
constant C : Type.{u}
check C -- C : Type.{u}
check A → C -- A.{l_1} → C : Type.{imax l_1 u}
universe variable v
constants D E : Type
check D → E -- D.{l_1} → E.{l_2} : Type.{imax l_1 l_2}
check D.{v} → E.{v} -- D.{v} → E.{v} : Type.{v}
命令universe u创建固定的Universe参数。 相反,在最后一个示例中,universe变量v仅用于将D和E放在同一类型的Universe中。 当D. {v}→E。{v}出现在更精细的上下文中时,Lean被约束为两者分配相同的Universe参数。
你现在不应该担心imax的含义。 宇宙的约束是微妙的,但好消息是精益处理得很好。 因此,在普通情况下,您可以忽略Universe参数,只需编写Type,将“Universe管理”留给Lean。
2.3 函数抽象和赋值
我们已经看到,如果我们有m n:nat,那么我们有对m n:nat×nat。 这为我们提供了一种创建自然数对的方法。 相反,如果我们有p:nat×nat,那么我们有pr1 p:nat和pr2 p:nat。 这为我们提供了一种通过提取其两个组件来“使用”一对的方法。
我们已经知道如何“使用”函数f:A→B,即我们可以将它应用于元素a:A来获得f a:B。但是我们如何从另一个表达式创建函数?
应用程序的伴侣是一个称为“抽象”或“lambda抽象”的过程。 假设通过暂时假设一个变量x:A,我们可以构造一个表达式t:B。然后表达式fun x:A,t,或等价地,λx:A,t,是A→B类型的对象。 这是从A到B的函数,它将任何值x映射到值t,这取决于x。 例如,在数学中,通常会说“让f为将任何自然数x映射到x + 5的函数”。 表达式λx:nat,x + 5只是该赋值右侧的符号表示。
import data.nat data.bool
open nat bool
check fun x : nat, x + 5
check λ x : nat, x + 5
以下是一些更抽象的例子:
constants A B : Type
constants a1 a2 : A
constants b1 b2 : B
constant f : A → A
constant g : A → B
constant h : A → B → A
constant p : A → A → bool
check fun x : A, f x -- A → A
check λ x : A, f x -- A → A
check λ x : A, f (f x) -- A → A
check λ x : A, h x b1 -- A → A
check λ y : B, h a1 y -- B → A
check λ x : A, p (f (f x)) (h (f a1) b2) -- A → bool
check λ x : A, λ y : B, h (f x) y -- A → B → A
check λ (x : A) (y : B), h (f x) y -- A → B → A
check λ x y, h (f x) y -- A → B → A
精益将最后三个例子解释为同一个表达; 在最后一个表达式中,Lean从f和h的类型推断出x和y的类型。
一定要尝试写一些自己的表达方式。 一些数学上常见的函数操作示例可以用lambda抽象来描述:
constants A B C : Type
constant f : A → B
constant g : B → C
constant b: B
check λ x : A, x -- the identity function on A
check λ x : A, b -- a constant function on A
check λ x : A, g (f x) -- the composition of g and f
check λ x, g (f x) -- (Lean can figure out the type of x)
-- we can abstract any of the constants in the previous definitions
check λ b : B, λ x : A, x -- B → A → A
check λ (b : B) (x : A), x -- equivalent to the previous line
check λ (g : B → C) (f : A → B) (x : A), g (f x)
-- (B → C) → (A → B) → A → C
-- we can even abstract over the type
check λ (A B : Type) (b : B) (x : A), x
check λ (A B C : Type) (g : B → C) (f : A → B) (x : A), g (f x)
想想这些表达意味着什么。例如,最后一个表示具有三种类型A,B和C以及两个函数g:B→C和f:A→B的函数,并返回g和f的组成。 (理解这个函数的类型需要理解依赖的产品,我们将在下面解释。)在lambda表达式λx:A,t中,变量x是一个“绑定变量”:它实际上是一个占位符,其“范围”不超出t。例如,表达式λ(b:B)(x:A)中的变量b,x与先前声明的常数b无关。实际上,表达式表示与λ(u:B)(z:A),z相同的函数。形式上,在重命名绑定变量之前相同的表达式称为alpha等效,并且被认为是“相同的”。精益认识到这种等同性。
请注意,将术语t:A→B应用于术语s:A会产生一个表达式t s:B。返回上一个示例并重命名绑定变量以便清楚,请注意以下表达式的类型:
constants A B C : Type
constant f : A → B
constant g : B → C
constant h : A → A
constants (a : A) (b : B)
check (λ x : A, x) a -- A
check (λ x : A, b) a -- B
check (λ x : A, b) (h a) -- B
check (λ x : A, g (f x)) (h (h a)) -- C
check (λ v u x, v (u x)) g f a -- C
check (λ (Q R S : Type) (v : R → S) (u : Q → R) (x : Q),
v (u x)) A B C g f a -- C
As expected, the expression (λ x : A, x) a
has type A
. In fact, more should be true: applying the expression (λ x : A, x)
to a
should "return" the value a
. And, indeed, it does:
constants A B C : Type
constant f : A → B
constant g : B → C
constant h : A → A
constants (a : A) (b : B)
eval (λ x : A, x) a -- a
eval (λ x : A, b) a -- b
eval (λ x : A, b) (h a) -- b
eval (λ x : A, g (f x)) a -- g (f a)
eval (λ v u x, v (u x)) g f a -- g (f a)
eval (λ (Q R S : Type) (v : R → S) (u : Q → R) (x : Q),
v (u x)) A B C g f a -- g (f a)
命令eval告诉Lean评估表达式。 将表达式(λx,t)s简化为t [s / x]的过程 - 即用s代替变量x的t - 被称为β减少,并且β减少到一个共同项的两个项是 称为beta等价物。 但是eval命令也会执行其他形式的缩减:
import data.nat data.prod data.bool
open nat prod bool
constants m n : nat
constant b : bool
print "reducing pairs"
eval pr1 (pair m n) -- m
eval pr2 (pair m n) -- n
print "reducing boolean expressions"
eval tt && ff -- ff
eval b && ff -- ff
print "reducing arithmetic expressions"
eval n + 0 -- n
eval n + 2 -- succ (succ n)
eval (2 : nat) + 3 -- 5
在后面的章节中,我们将解释如何评估这些术语。 目前,我们只想强调这是依赖型理论的一个重要特征:每个术语都有计算行为,并支持减少或归一化的概念。 原则上,减少到相同值的两个术语在定义上是相等的。 它们被底层逻辑框架视为“相同”,而精益则尽力识别并支持这些标识。
2.4 介绍定义
如上所述,在精益环境中声明常量是假设新对象进行实验的好方法,但大多数时候我们真正想要做的是在精益中定义对象并证明它们的相关信息。 definition命令提供了一种定义新对象的重要方法。
constants A B C : Type
constants (a : A) (f : A → B) (g : B → C) (h : A → A)
definition gfa : C := g (f a)
check gfa -- C
print gfa -- g (f a)
-- We can omit the type when Lean can figure it out.
definition gfa' := g (f a)
print gfa'
definition gfha := g (f (h a))
print gfha
definition g_comp_f : A → C := λ x, g (f x)
print g_comp_f
定义的一般形式是定义foo:T:= bar。 精益通常可以推断类型T,但明确地写它通常是个好主意。 这澄清了您的意图,如果定义的右侧没有正确的类型,精益将标记错误。
因为函数定义很常见,所以Lean提供了另一种表示法,它将抽象变量放在冒号之前并省略lambda:
definition g_comp_f (x : A) : C := g (f x)
print g_comp_f
净效果与之前的定义相同。
这里有一些定义的例子,这次是在算术的上下文中:
import data.nat
open nat
constants (m n : nat) (p q : bool)
definition m_plus_n : nat := m + n
check m_plus_n
print m_plus_n
-- again, Lean can infer the type
definition m_plus_n' := m + n
print m_plus_n'
definition double (x : nat) : nat := x + x
print double
check double 3
eval double 3 -- 6
definition square (x : nat) := x * x
print square
check square 3
eval square 3 -- 9
definition do_twice (f : nat → nat) (x : nat) : nat := f (f x)
eval do_twice double 2 -- 8
作为练习,我们鼓励你使用do_twice和double来定义四倍输入的函数,并将输入乘以8.作为进一步的练习,我们鼓励你尝试定义函数Do_Twice:((nat→nat)→( nat→nat))→(nat→nat)→(nat→nat)迭代其参数两次,以便Do_Twice执行一个迭代其输入四次的函数,并评估Do_Twice do_twice double 2。
上面,我们讨论了“currying”一个函数的过程,即取一个函数f(a,b),它将一个有序对作为一个参数,并将其重新形成为一个连续取两个参数的函数f'a b。 作为另一项练习,我们鼓励您完成以下定义,即“curry”和“uncurry”函数。
import data.prod
open prod
definition curry (A B C : Type) (f : A × B → C) : A → B → C := sorry
definition uncurry (A B C : Type) (f : A → B → C) : A × B → C := sorry