翻译-Lean Language Reference-04-类型系统
项(又称表达式)是构成 Lean 核心语言含义的基本单位。 它们是由阐述器 根据用户编写的语法生成的。 Lean 的类型系统会将项与它们的类型关联起来。 类型本身也是项,可以类比集合的表示,而 项则对应这些集合里的个体元素。 如果项的类型能通过基于 Lean 的类型理论规则的推理获得, 那么它就是类型良好的 Well-Typed。只有类型良好的项才有意义。
项是一种带依值类型的 λ-演算:它们包括
函数项的构造、
函数项的应用、
let 绑定、
标识符。
项语言中的标识符除了可以指代
受缚变量之外,还可以指代
归纳项的构造子、
归纳类型构造子、
归纳项的递归子、
不透明常量以及
已声明常量 Declared Constant、
其中前四者不可以被展开,而
已声明常量可以被展开为对应的定义内容。
一个项的类型良好性推导需要非常明确(显式)地指明所使用到的推理规则。 隐晦(隐式)地说,类型良好的项甚至可以直接充当其自身类型良好性的推导证明。 Lean 的类型理论足够清晰明确,以至于基于类型良好的项 我们可以直接重建其完整的类型推导过程。这 极大地减少了存储完整推导树所带来的系统开销,同时又 赋予 Lean 强大的表达能力来描绘现代数学研究。 这意味着,证明项就是定理真实性的充分证据, 并且极易于进行独立验证。
除了拥有类型之外,项还可以通过 定义等价性 Definitional Equality 彼此关联: 这是一种可以通过机器检验的关系, 可以在语法层面上将项等同起来——前提是它们拥有类似的计算行为。 定义等价性包括以下几种形式的归约 Reduction:
β(beta)
通过替换名称的受缚出现来应用被构造的函数表达式。
δ(delta)
将已声明常量的出现替换为其对应定义。
ι(iota)
对目标为归纳项构造子的归纳项递归子(即原始递归)进行归约。
ζ(zeta)
将被
let绑定的名称替换为其对应的定义。Quotient
在商类型的函数提升算子被应用于商元素时 对其进行归约。
被完全归约的项称为规范形式 Normal Form。
定义等价还涵盖了
函数以及
只有单个项构造子的归纳类型的
η 等价 η-Equivalence:比如说
函数项 fun x => f x 与 f 就是定义等价的,或者说
给定一个拥有字段 f1 和 f2 的结构类型下的项 S,
S.mk x.f1 x.f2 与 x 就是定义等价的。
它还满足命题类型具有证明无关性 Proof Irrelevance:
同一命题的任意两个证明都是定义等价的。
这种等价性具有自反性、对称性,但却没有传递性。
定义等价性可用于类型转换: 如果有两个项是定义等价的, 并且有一个额外的项以其中一个项作为其类型, 那么这个额外的项也可以以另一个作为其类型。 这是因为定义等价性涵盖了归约, 类型也可以通过对数据进行计算而得出。
函数 LengthList 在被应用于一个自然数实参时会计算出一个类型,
该类型对应于一个拥有具体元素数目的向量:
def LengthList (α : Type u) : Nat → Type u
| 0 => PUnit
| n + 1 => α × (LengthList α n)由于 Lean 的有序对是右结合的, 多个嵌套的括号可以被省略:
example : LengthList Int 0 := ()
example : LengthList String 2 :=
("Hello", "there", ())如果向量的长度与元素数量不匹配, 那么计算出的类型就无法与项匹配:
example : LengthList String 5 :=
("Wrong", "number", ())
-- Application type mismatch: The argument
-- ()
-- has type
-- Unit
-- but is expected to have type
-- LengthList String 3
-- in the application
-- ("number", ())Lean 里面基本的类型有
宇宙、
函数、
商类型构造子 Quot 以及
归纳类型构造子。
已声明常量、
归纳项的递归子的应用、
函数项应用、
公理、
不透明常量
也可以额外地产生类型,
正如它们能够产生其他类型下的项一样。
4.1. 函数 Functions
函数 Function 类型为 Lean 的内置功能。 函数会将 一种类型(定义域 Domain)下的值映射到 另一种类型(陪域 Codomain)下的值,并且 函数类型会指明函数的定义域和陪域。
函数类型有以下两种:
依值的 Dependent
依值函数的类型签名可以显式地声明形参,如此 类型签名里的陪域便可以显式地调用形参。 由于类型可以根据值计算得出, 因此基于不同的实参,依值函数 返回值的类型与数目均可能不同。
依值函数类型有时也被称为 依值积 Dependent Products, 因为它们对应于一族集合的索引积。
非依值的 Non-Dependent
非依值函数的类型签名不包含形参,并且 其陪域不会基于所提供的具体实参而变化。
函数 two 可能返回不同类型的值,
具体取决于它在应用时被传入的实参:
def two : (b : Bool) → if b then Unit × Unit else String :=
fun b =>
match b with
| true => ((), ())
| false => "two"函数体无法被写成 if … then … else … 的形式,
因为它不会像 match 那样对类型进行细分 RefineTODO。
在 Lean 的核心语言中, 所有函数类型都是依值的,也包括非依值函数 —— 只不过非依值函数的形参名称不会出现在陪域中。此外, 即便两个依值函数类型的形参命名不同, 它们也可以是定义等价的 —— 只要能够通过重命名形参使它们相等即可。 不过 Lean 的阐述器并不会为 非依值函数类型里的形参引入局部绑定(直接被归约了)。
非依值函数类型
(x : Nat) → String 和
Nat → String
是定义等价的:
example :
((x : Nat) → String) =
(Nat → String) :=
rfl类似地,依值函数类型
(n : Nat) → n + 1 = 1 + n 和
(k : Nat) → k + 1 = 1 + k
是定义等价的:
example :
((n : Nat) → n + 1 = 1 + n) =
((k : Nat) → k + 1 = 1 + k) :=
rfl要描述“数组中的所有元素都非零” 需要用到一个依值函数:
def AllNonZero (xs : Array Nat) : Prop :=
(i : Nat) → (lt : i < xs.size) → xs[i] ≠ 0
-- 这里 (lt : i < xs.size) 意味着依值函数类型表达式绑定了名称 lt这是因为负责数组访问的阐述器需要用到“数组索引在界内”的证明。 非依值版本的陈述不会引入这个假设:
def AllNonZero (xs : Array Nat) : Prop :=
(i : Nat) → (i < xs.size) → xs[i] ≠ 0
-- failed to prove index is valid, possible solutions:
-- - Use `have`-expressions to prove the index is valid
-- - Use `a[i]!` notation instead, runtime check is performed, and
-- 'Panic' error message is produced if index is not valid
-- - Use `a[i]?` notation instead, result is an `Option` type
-- - Use `a[i]'h` notation instead, where `h` is a proof that index is valid
-- xs:Array Nati:Nat
-- ⊢ i < xs.size虽然核心类型理论并不包含隐式参数, 但是函数类型确实包含了一个指示形参是否为隐式的标记。 尽管这个信息会被 Lean 的阐述器所使用,但是其 并不影响核心理论中的类型检查或定义等价性,并且 在仅分析核心类型理论的时候可以被忽略。
类型
{α : Type} → (x : α) → α 和
(α : Type) → (x : α) → α
是定义等价的,
即便首个形参
在一个当中是隐式的,而
在另一个里却是显式的:
example :
({α : Type} → (x : α) → α)
=
((α : Type) → (x : α) → α)
:= rfl4.1.1. 抽象 Function Abstraction
在 Lean 的类型理论中,函数的创建是通过 一个绑定名称的函数抽象表达式来完成的。
函数抽象在其他社群中也被称作 lambda, 源于 Alonzo Church 为其创建的记号; 又或者被称作是匿名函数, 因为它们不需要在全局环境中被命名。
当函数被应用时,表达式的结果是通过 β 归约 求得的: 此过程中实参会被拿去替换函数表达式里对应名称的受缚出现。 在编译后的代码中这个过程必须严格发生, 实参必须已经成为一个值; 然而在类型检查时就没有这样的限制, 与定义等价有关的方程理论 允许对任意项进行 β 归约。
在 Lean 的项语言中, 函数抽象表达式可以 声明多个形参,或者是 使用模式来匹配形参。 这些特性会被翻译为核心语言, 当中所有函数抽象都只接收一个实参。 并非所有函数都源于函数抽象: 归纳类型构造子、 归纳项构造子以及 归纳项的递归子 都可能具有函数类型, 但它们不能单独依靠函数抽象来创建。
4.1.2. 柯里化 Currying
在 Lean 的核心类型理论中, 每个函数都会将 定义域里的每个元素都映射到 陪域里的单个元素。 换句话说,每个函数都只期望接收一个实参。 多参函数是通过高阶函数的构造来实现的, 当提供第一个实参时, 它会返回一个新的函数,该函数期望接收剩余的实参。 这种编码方式被称为柯里化 Currying, 由 Haskell B. Curry 推广并以其命名。 Lean 里面关于 函数构造、 函数类型指定以及 函数项应用 的语法造就了一种多参函数的错觉, 但其实阐述的结果只有单形参函数。
4.1.3. 外延性 Extensionality
函数的定义等价性在 Lean 里面是内涵的 Intensional。 这意味着定义等价性是在语法层面上定义的——前提是忽略 项所绑定的名称的重命名以及 项的归约。 粗略地说,只要两个函数实现了相同的算法, 那么它们就是定义等价的; 这有别于数学中通常描述的相等——即 两个函数都能够将 定义域中的同一个元素映射到 陪域中的同一个元素上。
定义等价性会被类型检查器所使用,因此它必须是可被预测的。 内涵等价性的语法特性意味着对其进行检查的算法是可被切实可行地设计并规范化的。 外延等价性的检查本质上涉及到证明与函数相等有关的任意定理,并且 外延等价性的检查算法并不存在一个清晰的规范; 这使得其对于类型检查器是一个糟糕的选择。因此, 函数的外延等价性在这里被作为一种推理原则提供, 在涉及到证明两个函数相等的命题的时候就可以调用它。
除了项的归约以及项所绑定的名称的重命名以外,
定义等价性还支持一种有限形式的外延性,
即 项的 η 等价;
在这种等价中,函数等价于
在函数体中将自身应用到形参的抽象。举个例子:
给定 f,其具有类型为 (x : α) → β x,
那么 f 与 fun x => f x 就是定义等价的。
在对函数进行推理的时候,
定理 funext 或者
相应的策略 funext、ext
可被用于证明两个函数相等——
只要这两个函数能够将相等的输入映射到相等的输出即可。
不同于其他内涵性的类型理论,
funext在 Lean 里是一个定理。 它的证明可以通过商类型来完成。
funextfunext.{u, v} {α : Sort u} {β : α → Sort v} {f g : (x : α) → β x}
(h : ∀ (x : α), f x = g x) : f = g函数外延性 Function Extensionality: 对于每个可以接收的实参, 如果两个函数返回的结果都相等, 那么两个函数相等。
之所以称作“外延性”是因为其提供了一种 基于底层数学函数的性质来证明两个对象相等的方法,而不是 基于表示它们的语法。 函数外延性是一个定理, 可以通过商类型来证明。
4.1.4. 完全性、可以终止性 Totality and Termination
函数可以通过 def 来递归定义。
从 Lean 的逻辑角度来看,所有函数都是完全的 Total,
意味着它们会在有限时间内
将定义域中的每个元素
映射到陪域中的一个元素上。
某些编程语言社群在术语“完全”的使用上可能有差异。 在那里函数被视作是完全的, 只要它们能满足不会因为未处理的情况而崩溃即可。 不停机的要求则被忽略。
完全函数的在所有类型正确的实参下的值都是定义好的,并且 它们不会因为模式匹配中情况缺失而无法停机或崩溃。
尽管 Lean 的逻辑模型认为所有函数都是完全的, 但是 Lean 也是一种实用的编程语言,它提供了某些“逃生舱口”。 即便函数的可以终止性还未被证明, 它们仍然可以在 Lean 的逻辑中使用, 只要能够证明它们的陪域是非空的即可。 这些函数在 Lean 的逻辑中被视作是未解释的函数,并且 它们的计算行为会被忽略。 但是在编译后的代码中,这些函数就像其他函数一样被处理。 还有一些函数可能会被标记为不安全的 Unsafe; 这些函数在 Lean 的逻辑中是不可用的。 与部分函数和不安全函数定义有关的章节 介绍了更多关于递归函数编程的细节。
类似地,存在于编译后代码中的那些
会在运行时发生失败的操作(如数组的越界访问),
只有在已知结果类型是被占据的(即类型非空)的情况下才会被使用。
在 Lean 的逻辑里,这些操作的结果将会是该类型下任选的一个实例(具体来说是
由该类型的 Inhabited 类型类实例所指定的那个默认值)。
函数 thirdChar 提取数组的第 3 个元素;
如果数组的长度小于等于 2,那么会导致恐慌:
def thirdChar (xs : Array Char) : Char := xs[2]!#['!'] 和 #['-', 'x'] 不存在的第三个元素是相等的,
因为它们会得到同一个任意选择的字符:
example : thirdChar #['!'] = thirdChar #['-', 'x'] := rfl两个结果的确都等于 'A',
这恰好是 Char 的默认回退 Fallback 值:
example : thirdChar #['!'] = 'A' := rfl
example : thirdChar #['-', 'x'] = 'A' := rfl4.1.5. API 参考 API Reference
4.1.5.1. 相关操作 Operations
Function 命名空间包含了以下
用于处理函数的通用辅助工具:
Function.compFunction.comp.{u, v, w} {α : Sort u} {β : Sort v} {δ : Sort w}
(f : β → δ) (g : α → β) : α → δ函数复合运算,通常使用中缀运算符 ∘ 来表示。
一个新函数将由两个给定的函数创建,其中
一个函数的输出将作为另一个函数的输入。
例子:
Function.comp List.reverse (List.drop 2) [3, 2, 4, 1] = [1, 4](List.reverse ∘ List.drop 2) [3, 2, 4, 1] = [1, 4]
标识符命名约定:
∘的推荐写法是comp。
Function.constFunction.const.{u, v} {α : Sort u} (β : Sort v) (a : α) : β → α构造忽略实参的常值函数。
如果 a : α, 那么 Function.const β a : β → α 便是“输出值为 a 的常值函数”。
如此对于所有实参 b : β 都会有 Function.const β a b = a。
上述函数通常也可以直接写成 fun _ => a。
例子:
Function.const Bool 10 true = 10Function.const Bool 10 false = 10Function.const String 10 "any string" = 10
Function.curryFunction.curry.{u_1, u_2, u_3} {α : Type u_1} {β : Type u_2}
{φ : Sort u_3} : (α × β → φ) → α → β → φ将函数从接收一个二元组的形式 转换成为接收两个实参的等价形式。
例子:
Function.curry (fun (x, y) => x + y) 3 5 = 8Function.curry Prod.swap 3 "five" = ("five", 3)
Function.uncurryFunction.uncurry.{u_1, u_2, u_3} {α : Type u_1} {β : Type u_2}
{φ : Sort u_3} : (α → β → φ) → α × β → φ将函数从接收两个实参的形式 转换成为接收一个二元组的等价形式。
例子:
Function.uncurry List.drop (1, ["a", "b", "c"]) = ["b", "c"][("orange", 2), ("android", 3) ].map (Function.uncurry String.take) = ["or", "and"]
4.1.5.2. 相关谓词 Properties
Function.InjectiveFunction.Injective.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(f : α → β) : Prop函数 f 是单射的 Injective
当且仅当 f x = f y 蕴含 x = y。
Function.SurjectiveFunction.Surjective.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(f : α → β) : Prop函数 f : α → β 被称为满射的 Surjective
当且仅当对每个 b : β 都存在一个 a : α 使得 f a = b。
Function.LeftInverseFunction.LeftInverse.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(g : β → α) (f : α → β) : PropLeftInverse g f 表示函数 g 是函数 f 的左逆,即 g ∘ f = id。
Function.HasLeftInverseFunction.HasLeftInverse.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(f : α → β) : PropHasLeftInverse f 意味着
函数 f 存在一个左逆。
Function.RightInverseFunction.RightInverse.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(g : β → α) (f : α → β) : PropRightInverse g f 表示函数 g 是函数 f 的右逆,即 f ∘ g = id。
Function.HasRightInverseFunction.HasRightInverse.{u_1, u_2} {α : Sort u_1} {β : Sort u_2}
(f : α → β) : PropHasRightInverse f 意味着
函数 f 存在一个右逆。
4.2. 命题 Propositions
命题 Proposition 是指那些可以被证明的、
有意义的陈述。毫无意义的陈述不是命题,
但是类型为 False 的陈述是命题。
所有命题都被归类为 Prop。
命题有以下特性:
定义上的命题类型的证明无关性 Definitional Proof Irrelevance
同一命题的任意两个证明都是可互换的。
运行时无关性 Run-Time Irrelevance
命题的证明会在代码编译后被擦除。
命题可以量化任意宇宙层级下的类型。
受限消去 Restricted Elimination
除了子单例的情况以外, 任何命题都不能被消解到非命题类型中。
任意两个逻辑上等价的命题 都可以通过公理
propext被证明为彼此相等的。
propextpropext {a b : Prop} : (a ↔ b) → a = b公理 propext 断言:
如果两个命题 a 和 b 是逻辑上等价的
(即 a 可以从 b 证明得出,反之亦然),
那么它们便是相等的。
如此意味着 a 在语境中的所有出现都可被替换为 b。
标准的逻辑连词可以被证明遵守命题类型的外延性。
然而公理有时是必需的,比如
处理像 P a 这样的高阶表达式——其中 P : Prop → Prop 未知;或者是
处理等式。
命题类型的外延性在直觉主义视角下是有效的。
4.3. 宇宙 Universes
类型是通过宇宙 Universe 进行分类的。
宇宙也被称作是 Sort。
每个宇宙都有一个对应的层级 Level,层级是一个自然数。
Sort 运算符会通过提供的层级构造一个宇宙。
如果一个宇宙的层级小于另一个宇宙的层级,
那么该宇宙本身就会被认为是较小的。
除了命题(将在本章后面描述)之外,
每个宇宙中的类型都只能量化比自己小的宇宙中的类型。
Sort 0 为命题类型,而形如
Sort (u + 1) 的都是数据类型所处的宇宙。
每个宇宙都是上一层更大宇宙中的一个元素,
因此 Sort 5 包含了 Sort 4。这意味着
以下例子是可被接受的:
example : Sort 5 := Sort 4
example : Sort 2 := Sort 1但是 Sort 3 并不是 Sort 5 的一个元素(跨了一层):
example : Sort 5 := Sort 3
-- Type mismatch
-- Type 2
-- has type
-- Type 3
-- of sort `Type 4` but is expected to have type
-- Type 4
-- of sort `Type 5`Unit 的类型为 Sort 1,
因此出于同样的原因,
其也不是 Sort 2 的一个元素:
example : Sort 1 := Unit
example : Sort 2 := Unit
-- Type mismatch
-- Unit
-- has type
-- Type
-- of sort `Type 1` but is expected to have type
-- Type 1
-- of sort `Type 2`由于命题和数据类型的
使用方式以及
约束规则不同,
因此提供了缩写 Type 和 Prop 来方便地区分它们。
Type u 是 Sort (u + 1) 的缩写,如此有
Type 0 是 Sort 1 而
Type 3 是 Sort 4。
Type 0 也可以简写为 Type,如此有
Unit : Type 且
Type : Type 1。
Prop 则是 Sort 0 的缩写。
4.3.1. 直谓性 Predicativity
每个宇宙都包含依值函数类型, 依值函数类型还可以拿来表示全称量化和蕴含。 一个函数类型对应的宇宙是由其定义域和陪域的宇宙所决定的, 具体情况取决于函数的陪域是否为一个命题。
谓词是返回类型为命题的函数(即函数的陪域是 Prop):
它们接收的实参的类型可以取自任意宇宙,
但函数类型本身仍然属于 Prop;
这意味着命题具有非直谓 Impredicative 量化的特性,
因为命题本身可以是关于所有命题(以及其他所有类型)的陈述。
命题类型的证明无关性就可以被写成一个量化所有命题的命题:
example : Prop := ∀ (P : Prop) (p1 p2 : P), p1 = p2命题也可以量化任何宇宙层级下的类型:
example : Prop := ∀ (α : Type), ∀ (x : α), x = x
example : Prop := ∀ (α : Type 5), ∀ (x : α), x = x对于宇宙层级为 1 以上的宇宙(即 Type u 层级)
量化都是直谓的 Predicative。
对这些宇宙而言,函数类型的宇宙层级即
定义域宇宙层级和陪域宇宙层级的最大者。
下面两个函数类型的宇宙层级都是 Type 2:
example (α : Type 1) (β : Type 2) : Type 2 := α → β
example (α : Type 2) (β : Type 1) : Type 2 := α → β下述例子会报错,因为 α → β 的类型被指定为 Type 1,
而实际上计算出来的结果应该是 Type 2:
example (α : Type 2) (β : Type 1) : Type 1 := α → β
-- Type mismatch
-- α → β
-- has type
-- Type 2
-- of sort `Type 3` but is expected to have type
-- Type 1
-- of sort `Type 2`Lean 的宇宙并不是累积的;
Type u 中的类型并不会自动地也属于 Type (u + 1)。
每个类型恰好属于一个宇宙。
下述例子会报错,因为 α → β 的类型被指定为 Type 3,
而实际上计算出来的结果应该是 Type 2:
example (α : Type 2) (β : Type 1) : Type 3 := α → β
-- Type mismatch
-- α → β
-- has type
-- Type 2
-- of sort `Type 3` but is expected to have type
-- Type 3
-- of sort `Type 4`4.3.2. 多态性 Polymorphism
Lean 还支持宇宙多态性 Universe Polymorphism,这意味着 在 Lean 环境中声明的常量可以带有宇宙层级形参 Universe Parameters。 在常量被调用时,这些形参就可以由具体的宇宙层级实例化。 宇宙层级形参的声明写在常量名后一个点之后的花括号里面。
恒等函数会接收一个宇宙层级形参 u 。其类型签名如下:
id.{u} {α : Sort u} (x : α) : α宇宙层级变量 / 形参还可以出现在宇宙层级表达式中, 这些表达式在定义中提供了具体的宇宙层级。 在多态定义被具体的层级实例化时, 这些宇宙层级表达式也会被求值以得到具体的层级。
在下述例子中,Codec 所处的宇宙层级
比其所包含的类型的宇宙层级大 1:
structure Codec.{u} : Type (u + 1) where -- u + 1 即宇宙层级表达式
type : Type u
encode : Array UInt32 → type → Array UInt32
decode : Array UInt32 → Nat → Option (type × Nat)大多数宇宙层级实参都可以被 Lean 自动推断出来。
在下面的例子中,我们没有必要将类型标注为 Codec.{0},
因为 Char 的类型是 Type 0,所以 u 必为 0:
def Codec.char : Codec where -- 没必要写成 Codec.{0} where
type := Char
encode buf ch := buf.push ch.val
decode buf i := do
let v ← buf[i]?
if h : v.isValidChar then
let ch : Char := ⟨v, h⟩
return (ch, i + 1)
else
failure宇宙多态常量的声明实际上创建了一个模式化定义 Schematic Definition, 使其可以在不同层级的宇宙上进行实例化。但是 在不同层级的宇宙上实例化可能会创建彼此不兼容的值。
这一点可以通过下述例子中看出。在下面的例子中,
T 是一个非常平凡的宇宙多态函数,它总是返回 true。
set_option autoImplicit false
def T.{u} (_ : Nat) : Bool :=
(fun (α : Sort u) => true) PUnit.{u}
set_option pp.universes true
theorem test.{u, v} : T.{u} 0 = T.{v} 0 := rfl如果 T 是通过 opaque 声明的,
因此 Lean 无法通过展开定义来检查等价性。
尽管 T 的两个实例都具有相同的类型,
但是由于是在不同宇宙上实例化的,因此它们并不兼容。
set_option autoImplicit false
opaque T.{u} (_ : Nat) : Bool := -- 这里变了
(fun (α : Sort u) => true) PUnit.{u}
set_option pp.universes true
def test.{u, v} : T.{u} 0 = T.{v} 0 := rfl
-- Type mismatch
-- rfl.{?u.5}
-- has type
-- Eq.{?u.5} ?m.7 ?m.7
-- but is expected to have type
-- Eq.{1} (T.{u} 0) (T.{v} 0)被自动绑定的隐式形参会尽可能地支持宇宙多态性。 如下定义恒等函数:
set_option autoImplicit true
def id' (x : α) := x将导致如下类型签名:
#check id'
-- id'.{u} {α : Sort u} (x : α) : α另一方面,由于 Nat 位于宇宙 Type 0 中,
这个函数会自动为 α 生成一个具体的宇宙层级;
这是因为 m 被同时应用于 Nat 和 α 上,
因此两者必须具有相同的类型,也就必须在同一个宇宙中:
set_option autoImplicit true
partial def count [Monad m] (p : α → Bool) (act : m α) : m Nat := do
-- 这里会自动在 count 与 [Monad m] 之间插入 {m α}
if p (← act) then
return 1 + (← count p act)
else
return 0
#check count
-- count.{u_1} {m : Type → Type u_1} {α : Type} [Monad m] (p : α → Bool) (act : m α) : m Nat4.3.2.1. 层级表达式 Level Expressions
常量声明里对宇宙层级的描述不仅局限于 宇宙层级变量以及 宇宙层级常量的加法。 宇宙间更复杂的关系 可以通过层级表达式来定义:
Level ::=
| 0 | 1 | 2 | ... -- 宇宙层级常量
| u, v -- 宇宙层级变量
| Level + n -- 宇宙层级表达式的运算
| max Level Level -- 宇宙层级的最小上界
| imax Level Level -- 宇宙层级的非直谓最小上界宇宙层级表达式的求值也遵循普通的算术规则。
操作 imax 的定义如下:
$\qquad\texttt{imax}~u~v=\begin{cases} 0 & \text{when } v = 0 \\ \max~u~v & \text{otherwise} \end{cases}$
imax 被用于实现 Prop 的非直谓量化。特别地,
如果有 A : Sort u 且 B : Sort v,
那么有 (x : A) → B : Sort (imax u v)。
如果有 B : Prop,
那么该函数类型下所有的项都位于 Prop 中;
否则该函数类型所处宇宙层级就是 u 和 v 的最大值。
4.3.2.2. 层级形参声明 Universe Variable Bindings
宇宙多态的常量声明会绑定名称作为宇宙层级形参的声明。
这些绑定可以是显式的,也可以是隐式的。
显式的宇宙层级名称绑定和实例化会在常量声明中
作为待声明常量的后缀出现:声明宇宙层级形参需要
在待声明常量名称后面加上一个点(.),然后
在花括号内放置以逗号分隔的宇宙层级变量序列。
常量 map 的定义
声明了两个宇宙层级形参(u 和 v)并
依次用它们实例化了多态的 List:
-- 这里的 List.{u} 和 List.{v} 是宇宙层级形参的显式调用
def map.{u, v} {α : Type u} {β : Type v} (f : α → β) : List.{u} α → List.{v} β
| [] => []
| x :: xs => f x :: map f xs正如 Lean 会自动实例化隐式形参,
Lean 也会自动实例化宇宙层级形参。
如果 autoImplicit 选项被设置为默认值 true,
那么隐式形参自动插入被启用,也就没必要显式地绑定宇宙层级变量,它们会被自动插入;
如果 autoImplicit 被设置为 false,
那么就必须显式地添加它们,或者是使用 universe 命令来声明它们。
当 autoImplicit 为 true(默认值)时,
下述定义会被接受,即便其没有绑定其宇宙层级形参:
set_option autoImplicit true
def map {α : Type u} {β : Type v} (f : α → β) : List α → List β
| [] => []
| x :: xs => f x :: map f xs当 autoImplicit 为 false 时,
下述定义会报错,因为 u 和 v 不在作用域内:
set_option autoImplicit false
def map {α : Type u} {β : Type v} (f : α → β) : List α → List β
| [] => []
| x :: xs => f x :: map f xs
-- unknown universe level `u`
-- unknown universe level `v`除了使用 autoImplicit 之外,
还可以使用 universe 命令在特定的区段作用域中声明宇宙层级变量。
command ::= ...
| universe ident ident*在当前作用域的范围内 声明一个或多个宇宙层级变量。
正如 variable 命令会使得
特定的标识符被视作具有特定类型的形参,
命令 universe 会使得
后续的标识符在声明中被隐式地量化为宇宙层级形参,
即便选项 autoImplicit 为 false。
set_option autoImplicit false
universe u
def id₃ (α : Type u) (a : α) := a由于自动隐式形参功能只会插入那些
在声明的头部中被使用到的参数,
所以那些仅出现在定义的右侧的宇宙层级变量不会被作为参数插入,
除非它们已经被声明为宇宙层级变量——即便 autoImplicit 被设置为 true。
下述带有宇宙层级形参显式声明的定义是可被接受的:
def L.{u} := List (Type u)即便是开启了隐式形参自动插入, 下述定义依然会被拒绝:
set_option autoImplicit true
def L := List (Type u)
-- unknown universe level `u`这是因为 u 没有在 := 之前的头部中被提及。下面的代码可以正常运行:
set_option autoImplicit true
def L : Type (u + 1) := List (Type u)添加宇宙层级变量声明后,u 就可以作为形参
在 := 的右侧被调用了:
universe u
def L := List (Type u)如此 L 的声明就是宇宙多态的,
其中 u 被作为宇宙层级形参插入。
在 universe 命令作用域范围内的声明都不会是多态的——
前提是宇宙层级变量
不在这些声明中出现,或者是
不在其他自动插入的实参中出现。
universe u
def L := List (Type 0)
#check L4.3.2.3. 提升 Universe Lifting
当一个类型的宇宙层级比在某些语境中预期的宇宙层级要小的时候,宇宙提升 Universe Lifting 操作符就可以弥合这个差距。 它们包裹着给定类型的项。相比于被包裹的类型,它们位于高层的宇宙中。 有两个提升操作符:
PLift结构类型构造子:
PLift.{u} (α : Sort u) : Type u将类型所处的宇宙层级往上提升一层。
PLift α 包裹了类型 α 下居留的证明或者值。
由此产生的类型将位于比 α 所在的宇宙高一层的宇宙中。
如此命题便可借此转为数据类型。
与之相关的类型 ULift 可以将非命题类型提升任意层级。
结构项构造子:
PLift.up.{u}
包裹一个证明或值,将其所居留的类型所处的宇宙层级提升一层。
结构字段:
down : α
从一个被抬升的项中提取出其所包裹的项。
例子:
#check False
-- False : Prop
#check PLift False
-- PLift False : Type
#check Nat
-- Nat : Type
#check (PLift Nat : Type 1)
-- PLift Nat : Type 1
#check (
[.up (by trivial), .up (by simp), .up (by decide)]
: List (PLift True) -- 这个很重要,不然 (by …) 会报错
)
-- [
-- { down := True.intro },
-- { down := True.intro },
-- { down := ⋯ }
-- ] : List (PLift True)ULift结构类型构造子:
ULift.{r, s} (α : Type s) : Type (max s r)将一个数据类型抬升到更高的宇宙层级。
ULift α 包裹了一个类型为 α 的值。
它不再占据与 α 相同的宇宙(即其可能所处宇宙层级的最小值),
而是接收一个额外的层级参数,并占据它们的最大值(max s r)。
由此产生的类型可以占据任何至少与 α 的宇宙一样大的宇宙。
该抬升算子最终生成的宇宙层级由第一个参数(即 r)决定,
你可以显式地写出该参数,同时让 α 的层级由系统自动推导。
与之相关的类型 PLift 可用于将命题或数据类型抬升一个层级。
结构项构造子:
ULift.up.{r, s}
将项所居留的类型所处的宇宙层级往上提升。
结构字段:
down : α
从被抬升的项中提取出其所包裹的项。
例子:
#check Nat
-- Nat : Type
#check (ULift Nat : Type 0)
-- ULift Nat : Type
#check (ULift Nat : Type 1)
-- ULift Nat : Type 1
#check (ULift Nat : Type 5)
-- ULift Nat : Type 5
#check (ULift.{7} (PUnit : Type 3) : Type 7)
-- ULift PUnit : Type 74.4. 归纳类型 Inductive Types
归纳类型 Inductive Type 是 Lean 中引入新类型的主要手段。 除了宇宙、函数类型和商类型是用户无法添加的内置语法以外, Lean 中的其他所有类型要么是 归纳类型,不然就是 基于宇宙、函数、归纳类型所构造的。 归纳类型由其类型构造子 Type Constructor 以及项构造子 Constructor 所指定。 归纳类型的其余属性皆由此二者推导而来。 每个归纳类型都有一个单一的类型构造子, 归纳类型构造子可以接受宇宙参数和普通参数。 每种归纳类型都可以拥有任意数量的项构造子, 这些归纳项构造子引入了新的值, 这些值的类型都以对应归纳类型的类型构造子开头。
基于归纳类型的类型构造子和项构造子, Lean 会推导出一个项递归子 Recursor。 从逻辑的角度观察,归纳项递归子代表归纳原理或归纳类型消去规则; 从计算的角度观察,它们对应于原始递归计算。 递归函数的可以终止性是通过将它们翻译为归纳项递归子的使用来证明的,因此 要做到这一点 Lean 的内核只需对归纳项递归子的应用进行类型检查即可,无需再进行额外的可以终止性分析。 此外 Lean 还会基于归纳项递归子生成一些辅助构造,这些构造会在系统的其他地方使用。
即便是对于非递归类型, 我们也会使用“归纳项递归子”这个术语。
结构类型是归纳类型的一个特殊情况,它们只有一个归纳项构造子。 在结构类型被声明之后,Lean 会生成一些辅助构造, 这些构造使得新的结构类型可以使用额外的语言特性。
本小节将会介绍: 归纳类型和结构类型声明的语法细节、 归纳类型声明后引入到环境中的新常量和新定义,以及 归纳类型下的值在编译后代码中的运行时表示。
4.4.1. 归纳类型声明 Inductive Type Declarations
command ::= ...
| declModifiers
inductive declId optDeclSig where
(| declModifiers ident optDeclSig)*
(deriving ident,*)?归纳类型的声明。declModifiers 的语法在
“声明修饰器”
一节中有详细介绍。
在声明一个归纳类型之后,其 类型构造子、 项构造子以及 项递归子 就会被引入到当前环境中。 新的归纳类型扩展了 Lean 的核心逻辑, 这些归纳类型的编码或表示并不是通过某些已有的数据类型实现的。 归纳类型的声明必须满足一系列形式良好性的要求, 以确保逻辑保持一致。
声明的第一行 —— 也就是从 inductive 开始到 where 结束的部分,
指定了新的归纳类型构造子的名称和类型。
如果有为归纳类型构造子提供类型签名,
那么归纳类型构造子的返回类型必须是一个宇宙,但是形参不必是类型。
如果没有提供类型签名,
那么 Lean 会尝试推导出一个足够大的宇宙来包含归纳类型构造子的返回类型。
不过这个过程在某些情况下可能会因为
无法找到最小的宇宙,或者是
根本找不到宇宙
而失败。因此注解有时是必不可少的。
归纳项构造子的具体描述在 where 之后。
归纳项构造子并不是必须的,比如归纳类型 False 和 Empty 就没有归纳项构造子。
每个归纳项构造子的指定
都以一个竖线(|,Unicode VERTICAL BAR (U+007c))开启,
然后跟着声明修饰器以及归纳项构造子的名称。
归纳项构造子名称是一个原始标识符。
归纳项构造子名称后可以补充归纳项构造子的类型签名,
类型签名可以
指定任何形参(前提是满足归纳类型声明形式良好性的要求),
但是其中返回的类型
必须是当前正在声明的归纳类型的类型构造子的饱和应用。
如果没有提供类型签名,
那么 Lean 便会自动推导归纳项构造子的类型 —— 通过
自动声明足够的隐式形参来构造一个形式良好的返回类型。
新的归纳类型名称会在当前命名空间中 被定义。 每个归纳项构造子的名称都位于归纳类型对应的命名空间内。
4.4.1.1. 形参和索引声明 Parameters and Indicies
归纳类型构造子可以接收以下两种类型的参数:形参 Parameter 和索引 Index。 在归纳类型声明中, 形参的调用必须保持一致, 各个归纳项构造子里所有的归纳类型构造子的应用都必须接收完全相同的实参。 索引作为实参 在归纳类型构造子的各次应用里可以各不相同。 在归纳类型构造子类型签名中,所有形参都必须出现在所有索引前面。
在归纳类型构造子的类型签名里,
出现在冒号(:)前面的形参会被视作是整个归纳类型声明的形参。
它们在归纳类型声明中的调用必须始终保持一致;而
出现在冒号后的形参则都是索引,
它们作为实参在归纳类型的声明中可以有所不同。但是,
如果选项 inductive.autoPromoteIndices 被设置为 true,
那么那些可以成为形参的索引就会被提升为形参。
要想升级索引为形参,需要满足
索引依赖的所有类型本身也是形参,并且在所有归纳类型构造子的调用里
索引始终是作为未被实例化的变量被一致地使用。
inductive.autoPromoteIndices默认值:true。
将归纳类型构造子的索引提升为形参,不论是否可行。
索引相当于是声明了一个类型族 Family of Types。 每个对索引的选择都会从这一族类型中选中一个类型, 被选中的这个类型有自己的一组可用的归纳项构造子。 带有索引的归纳类型构造子相当于指定了一个类型索引族 Indexed Families of Types。
4.4.1.2. 示范 Example Inductive Types
类型 Vacant 是一个空的归纳类型,
等价于 Lean 的 Empty 类型:
inductive Vacant : Type where空的归纳类型并不是没有用的; 它们可用于表示不可达的代码。
命题 No 是一个空的归纳类型,
等价于 Lean 的 False 命题。
inductive No : Prop where类型 Solo 等同于 Lean 的 Unit 类型:
inductive Solo where
| solo刚才的归纳类型声明
省略了归纳项构造子和归纳类型构造子的类型签名。
Lean 会将 Solo 赋值为 Type:
#check Solo
-- Solo : Type归纳项构造子被命名为 Solo.solo,
因为归纳项构造子名称位于归纳类型构造子的命名空间中。
由于 Solo 不需要任何参数,
因此 Solo.solo 的类型签名应被推导为:
#check Solo.solo
-- Solo.solo : Solo命题 Yes 等价于 Lean 的 True 命题:
inductive Yes : Prop where
| intro与 One 有所不同的是,
新的归纳类型 Yes 位于宇宙 Prop 中。
#check Yes
-- Yes : PropYes.intro 的类型签名将被推导为:
#check Yes.intro
-- Yes.intro : Yes类型 EvenOddList α b 下的项是一个列表,其中
α 是列表中元素的类型,而
b 为 true 当且仅当列表中有偶数个元素:
set_option autoImplicit true -- 没有这个会报错:unknown universe u
set_option relaxedAutoImplicit true -- 没有这个会报错:unknown identifier `isEven`
inductive EvenOddList (α : Type u) : Bool → Type u where
| nil : EvenOddList α true
| cons : α → EvenOddList α isEven → EvenOddList α (not isEven)
-- cons 这里会自动添加 {isEven}下方例子是类型良好的,因为列表中有两个元素:
example : EvenOddList String true :=
.cons "a" (.cons "b" .nil)下方例子不是类型良好的,因为列表中有三个元素:
example : EvenOddList String true :=
.cons "a" (.cons "b" (.cons "c" .nil))
-- Type mismatch
-- EvenOddList.cons "a" (EvenOddList.cons "b" (EvenOddList.cons "c" EvenOddList.nil))
-- has type
-- EvenOddList String !!!true
-- but is expected to have type
-- EvenOddList String true在这个声明中,
α : Type u 是一个形参,因为它在 EvenOddList 的所有出现中都被一致地调用;而
b : Bool 是一个索引,因为不同的 Bool 值在不同的出现中被使用。
在归纳类型构造子 Either 的类型签名中,所有形参都在冒号前面被声明。
set_option autoImplicit true
inductive Either (α : Type u) (β : Type v) : Type (max u v) where
| left : α → Either α β
| right : β → Either α β下面的版本中出现了两个名为 α 但可能不相同的类型:
set_option autoImplicit true
inductive Either' (α : Type u) (β : Type v) : Type (max u v) where
| left : {α : Type u} → {β : Type v} → α → Either' α β
| right : β → Either' α β
-- Mismatched inductive type parameter in
-- Either' α β
-- The provided argument
-- α
-- is not definitionally equal to the expected parameter
-- α✝
--
-- Note: The value of parameter `α✝` must be fixed
-- throughout the inductive declaration. Consider
-- making this parameter an index if it must vary.4.4.1.3. 项的匿名构造 Anonymous Constructor Syntax
如果一个归纳类型只有一个归纳项构造子,
那么这个归纳项构造子的调用就可以使用匿名表示 Anonymous Constructor Syntax。
与其写出归纳项构造子完整名称并将其应用于实参,
不如将由逗号分隔的项构造子实参包裹进一对尖括号(⟨ 和 ⟩,Unicode
MATHEMATICAL LEFT ANGLE BRACKET (U+0x27e8) 和
MATHEMATICAL RIGHT ANGLEBRACKET (U+0x27e9))。
这对于模式匹配和表达式语境都是可行的。如果是
打算按名称提供实参,或者是
打算用 @ 将所有隐式形参转为显式形参,
那么就必须使用普通的归纳项构造子语法。
归纳项构造子可以被匿名调用,只需将 它们的实参用逗号分隔并 包裹在一对尖括号内即可,
term ::= ...
| ⟨ term,* ⟩类型 AtLeastOne α 类似 List α,但总是非空:
inductive AtLeastOne (α : Type u) : Type u where
| mk : α → Option (AtLeastOne α) → AtLeastOne α归纳项构造子调用的匿名表示语法可以用在这里,并且可以用来对归纳项进行模式匹配:
def oneTwoThree : AtLeastOne Nat :=
⟨1, some ⟨2, some ⟨3, none⟩⟩⟩
def AtLeastOne.head : AtLeastOne α → α
| ⟨x, _⟩ => xLean 在背后会将归纳项构造子的匿名调用翻译为对等的传统的归纳项构造子写法:
def oneTwoThree' : AtLeastOne Nat :=
.mk 1 (some (.mk 2 (some (.mk 3 none))))
def AtLeastOne.head' : AtLeastOne α → α
| .mk x _ => x4.4.1.4. 实例推导 Deriving Instances
归纳类型声明中可选的 deriving 子句
可以用于推导类型类的实例。
更多信息请见实例推导一节。
4.4.2. 结构类型声明 Structure Declarations
command ::= ...
| declModifiers
structure declId bracketedBinder* (: term)?
(extends (ident : )?term,*)?
where
(declModifiers ident ::)?
structFields
(deriving derivingClass,*)?会声明一个新的结构类型。
结构类型也是归纳类型,只不过 结构类型只有单个归纳项构造子且没有索引。 作为对这些限制的交换,Lean 会为结构类型生成对应的代码; 这些代码提供了不少便利: 为各个字段生成对应的项投影函数 Projection; 提供了基于字段名称而非位置实参的额外的归纳项构造子应用语法,并且 类似的语法也可被用来更新结构项某些字段的值;然后 结构类型也可以用于扩展其他结构类型。 正如归纳类型,结构类型也可以是递归的; 它们也受到严格正性 Strict Positivity 有关的相同限制。 结构类型并没有为 Lean 增加任何表达能力; 它们的所有特性都是通过代码生成实现的。
4.4.2.1. 形参声明 Structure Parameters
与普通的归纳声明类似,结构声明的头部包含 可以指定结构类型构造子形参的类型签名,以及 生成的项所属的宇宙。 结构类型不能被用于定义类型索引族。
4.4.2.2. 字段声明 Fields
结构声明中的每个字段 Field 都对应于一个结构项构造子的形参。
结构类型构造子 MyProd 类似于 Prod:
set_option autoImplicit true
structure MyProd (α β : Type _) where
fst : α
snd : β结构类型构造子的两个形参以及结构的两个字段都是结构项构造子的形参:
MyProd.mk.{u, v}
{α : Type u}
{β : Type v}
(fst : α)
(snd : β)
: MyProd.{u, v} α β此外本例中的结构项构造子还是宇宙多态的。
结构类型构造子 MyProd 接收两个宇宙层级形参:
MyProd.{u, v} (α : Type u) (β : Type v) : Type (max u v)每个字段对应类型所属的宇宙层级
必须小于等于结构类型所处的宇宙层级。
Lean 会推导出足以容纳 Type u 和 Type v 的最小宇宙 Type (max u v)。
隐式形参自动声明功能会为每个字段单独插入隐式形参, 即便它们的名称可能存在冲突。 这些字段会成为结构项构造子的形参, 这些形参会对(字段的值的)类型进行量化。
对于结构类型 MyStructure 的所有字段,
它们的类型都被自动插入隐式形参:
structure MyStructure where
field1 : Fin n
field2 : Fin n结构项构造子 MyStructure.mk 里每个字段
都拥有自己的隐式形参 n : Nat:
MyStructure.mk
(field1 : {n : Nat} → Fin n)
(field2 : {n : Nat} → Fin n)
: MyStructure结构类型构造子 MyStructure 不接收任何形参,
返回的类型为 Type,而这正是 Nat 和 Fin n 所在的宇宙:
MyStructure : TypeLean 会为每个字段都生成一个项投影函数, 该函数会从结构项构造子那里提取字段对应的值。 这个函数位于与结构同名的命名空间中。 结构项投影函数会被阐述器特殊处理(如结构继承一节所描述的那样), 阐述器会执行额外的步骤而不仅仅只是查找命名空间而已。 当字段类型依赖于先前的字段时,依值的结构项投影函数 会用先前的项投影函数来书写,而不是直接 使用显式的模式匹配。
结构类型构造子 ArraySized 包含一个特殊字段 size_eq_length,
其类型同时依赖于结构类型构造子的形参 length 以及先前的结构字段 array:
set_option autoImplicit true
structure ArraySized (α : Type u) (length : Nat) where
array : Array α
size_eq_length : array.size = length项投影函数 size_eq_length 的类型签名
接收结构类型构造子的形参作为隐式形参,并且
使用合适的项投影函数来获取先前结构字段的值:
ArraySized.size_eq_length.{u}
{α : Type u} {length : Nat}
(self : ArraySized α length)
: self.array.size = length结构字段可以拥有默认值,
这是通过 := 指定的。
这些值会在没有提供显式值时使用。
一个有向图的邻接表表示可以被表示为一个 Nat 列表的数组。
数组的大小表示顶点的数量,并且
每个顶点的出边都存储在数组中该顶点的索引处。
由于字段 adjacency 提供了默认值 #[],
因此可以在不提供任何字段值的情况下构造空图 Graph.empty。
structure Graph where
adjacency : Array (List Nat) := #[]
def Graph.empty : Graph := {}结构字段也可以通过索引访问,使用点记法。 字段编号从 1 开始算起。
4.4.2.3. 项构造子 Structure Constructors
结构项构造子可以被重命名,只需
提供新名称以及 :: 即可。
如果没有显式地提供名称,
那么结构项构造子在结构对应的命名空间中就会被默认命名为 mk。
你还可以为结构项构造子的新名称补充额外的
声明修饰器。
结构类型 Palindrome 包含一个字符串以及一个证明,
后者用于证明字符串是一串回文:
structure Palindrome where
ofString ::
text : String
is_palindrome : text.toList.reverse = text.toList在这里结构项构造子被命名为
Palindrome.ofString 而不是
Palindrome.mk。
结构类型 NatStringBimap 维护了
自然数和字符串之间的一个有限双射:其由一对哈希映射组成,满足
每个映射的键在另一个映射中都能作为值恰好出现一次。
由于结构项构造子是私有的,定义所处的模块外部的代码
无法构造新的实例,
必须使用提供的 API 来维护类型的不变量。
此外,显式地提供结构项构造子默认名称
其实是借机给结构项构造子附加一段文档注释。
structure NatStringBimap where
/--
Build a finite bijection between some
natural numbers and strings
-/
private mk ::
natToString : Std.HashMap Nat String
stringToNat : Std.HashMap String Nat
def NatStringBimap.empty : NatStringBimap := ⟨{}, {}⟩
def NatStringBimap.insert
(nat : Nat) (string : String)
(map : NatStringBimap) :
Option NatStringBimap :=
if map.natToString.contains nat ||
map.stringToNat.contains string then
none
else
some <|
NatStringBimap.mk
(map.natToString.insert nat string)
(map.stringToNat.insert string nat)由于结构其实就是只有单个项构造子的归纳, 结构项构造子也可以通过归纳项构造子调用的匿名表示语法来调用或模式匹配。 结构项的构造或匹配也可以使用实例表示 Instance Notation 方式, 该表示法需要写出结构字段的名称以及对应的值。
term ::= ...
| { structInstField,*
(: term)? }基于用户提供的各个字段的值来构造结新的构新项。 字段指定器可以有两种形式:
structInstField ::= ...
| structInstLVal := private? termstructInstLVal 可以是
一个字段名称(标识符),
一个字段索引(自然数),或者是
一个方括号中的项,
然后再尾随零个或多个子字段。子字段要么是
一个带点号(.)前缀的字段名称或索引,或者是
一个方括号中的项。
这个语法会被阐述为结构项构造子的应用。 为字段提供的值是按名称给出的, 这些值可以按任意顺序提供。 为子字段提供的值则被用于初始化那些 本身位于其他字段内部的结构项构造子的字段。 结构的构造不允许使用方括号中的项;这些项只用于结构的更新。
structInstField ::= ...
| ident不包含 := 的字段指定器是字段缩写 Field Abbreviation。在这种语境下,
标识符 f 是 f := f 的缩写;也就是说,
当前作用域中 f 的值会被拿来初始化字段 f。
所有无默认值的字段都必须被提供值。 如果一个策略被指定为默认值, 那么它在阐述时会被运行以构造出参数的值。
在模式匹配的语境下, 字段名称会被映射到与其对应的项投影函数匹配的模式,而 字段缩写会被绑定一个模式变量,变量名称即为字段名称。 默认的实参仍然会出现在模式中; 如果一个模式没有为具有默认值的字段指定值, 那么这个模式只会匹配该字段的默认值。
如果字段声明包含修饰符 private,
那么该值会被置于当前模块的私有作用域中,即便结构的值本身位于公有作用域中。
该值会被包裹在一个公有但不被暴露的辅助定义中。
这在处理类型类的实例时尤其有用,
因为类型类公有实例里方法的实现默认是被暴露的。
这个修饰符可将它们设置为私有的。
可选的类型注解允许在 Lean 无法确定结构类型的语境中指定结构类型。
结构 AugmentedIntList 包含
一个列表 list 以及
一些额外信息 augmentation。
如果后者省略则为空:
structure AugmentedIntList where
list : List Int
augmentation : String := ""在测试列表是否为空时,函数 isEmpty
必须显式地匹配字段 augmentation,
即便该字段有默认值:
def AugmentedIntList.isEmpty : AugmentedIntList → Bool
| {
list := [],
augmentation := "" -- 不能省略此行,会报错
} => true
| _ => false
#eval {
list := [],
augmentation := "extra"
: AugmentedIntList
}.isEmpty
-- false即便结构声明是公有的,
单个字段也可以通过字段级别的 private 修饰符来隐藏。
在下述模块中,暴露的公有常量声明 x 允许使用私有定义 secret,
因为字段 imaginary 的值并没有被暴露:
module
public structure Complex where
real : Float
imaginary : Float
private def secret := 2.3
@[expose]
public def x : Complex := {
real := 5.0
imaginary := private 2 * secret
}在下述模块中,尽管 State 是公有的,
但是其结构项构造子和字段却都是私有的。
函数 State.toString 也是私有的,
并且打算通过 ToString 实例来访问;但是,
由于方法的实现会被暴露给公有实例,
因此会导致报错:
module
public structure State where
private mk ::
private count : Nat
private def State.toString (s : State) : String :=
s!"⟨{s.count}⟩"
public instance : ToString State where
toString s := s.toString
-- Invalid field `toString`: The environment
-- does not contain `State.toString`, so
-- it is not possible to project the field `toString`
-- from an expression
-- s
-- of type `State`
--
-- Note: A private declaration `State.toString`
-- (from the current module) exists but
-- would need to be public to access here.将 toString 的调用标记为私有
会将其从模块的公有作用域中移除,
从而使其可以访问私有函数:
module
public structure State where
private mk ::
private count : Nat
private def State.toString (s : State) : String :=
s!"⟨{s.count}⟩"
public instance : ToString State where
toString s := private s.toStringterm ::= ...
| {term with
structInstField,*
(: term)?}用于更新结构项。
with 前面的项应为一个结构项;
其为待更新的值。结构新实例会被创建,当中
未被指定的字段都会从待更新的值中复制过来,而
已被指定的字段则会被替换为它们的新值。
在更新结构时,我们也可以通过
在方括号中包含数组中待更新元素的索引来更新数组。
这种更新并没有强制要求
索引表达式在数组中是有效的,并且
索引越界的更新会被舍弃。
结构更新可以通过指定待更新的字段名称来更新字段。 索引越界的更新将会被忽略。
structure AugmentedIntArray where
array : Array Int
augmentation : String := ""
deriving Repr
def one : AugmentedIntArray := {array := #[1]}
def two : AugmentedIntArray := {one with array := #[1, 2]}
def two' : AugmentedIntArray := {two with array[0] := 2}
def two'' : AugmentedIntArray := {two with array[99] := 3}
#eval (one, two, two', two'')
-- ({ array := #[1], augmentation := "" },
-- { array := #[1, 2], augmentation := "" },
-- { array := #[2, 2], augmentation := "" },
-- { array := #[1, 2], augmentation := "" })结构项也可以通过 where 来声明,
后面跟着每个字段的值。这种方式
只能用于常量声明,不能用于表达式语境中。
where 表示Lean 中的积类型是一个名为 Prod 的结构。
积类型可以通过向 Prod 各个字段提供值来构造:
def location : Float × Float where
fst := 22.807
snd := -13.9234.4.2.4. 结构继承 Structure Inheritance
结构或许会被声明为继承自其他结构。
这是通过可选的 extends 子句来实现的。
如此声明后所产生的新结构会拥有所有父结构对应的所有字段。
如果父结构的字段名称有重叠,那么所有重叠的字段名称都必须具有相同的类型。
新结构将会拥有一个字段解析顺序 Field Resolution Order,
其会影响字段的值。如果可能的话,这个解析顺序将会是父结构的
C3 线性化 C3 Linearization。
特别地,字段解析顺序应该是所有父结构构成的集合上的一个全序关系,
满足每个 extends 的链都是有序的。
如果 C3 线性化无解,那么 Lean 就会利用启发式方法来找到一个顺序。
每个结构在其自身的字段解析顺序中都是排名第一的。
字段解析顺序会被用于计算可选字段的默认值。 当字段的值未被指定时,默认值将会按照解析顺序中的首个类进行设置。 字段默认值表达式中的字段引用同样也遵循字段解析顺序;这意味着: 如果子结构声明覆盖了父结构字段的默认值, 那么其也可能会改变父结构字段的默认值。 由于子结构在其自身解析顺序中排名第一, 因此子结构中字段的默认值优先于父结构的默认值。
当一个新结构继承了现有的结构时, 新结构对应的项构造子会将现有结构的信息作为额外的实参接收。通常情况下这体现为 所有父结构都会有一个对应的项作为实参被传入新结构对应的项构造子, 每个父结构对应的项都携带了父结构所有字段的值。然而, 如果多个父结构的字段存在重叠,那么新结构对应的项构造子就 只会包含来自一个或多个父结构不重叠字段的子集, 而非父结构下完整的项,以此来避免信息重复。
这里不存在父结构与子结构之间的子类型关系。
即便结构类型 B 继承了结构类型 A,
一个期望实参类型为 A 的函数也不会接受 B 类型下的项。但是,
转换函数将会被生成,用于将结构项转换为它的每个父结构对应的项。
这些转换函数称作是项的父投影函数 Parent Projection。
项的父投影函数位于与子结构类型同名的命名空间中,
它们的名称为父结构类型名称前加上前缀 to。
在这个例子中,Textbook 是一个 Book,同时也是一个 AcademicWork:
structure Book where
title : String
author : String
structure AcademicWork where
author : String
discipline : String
structure Textbook extends Book, AcademicWork
#check Textbook.toBook
-- Textbook.toBook (self : Textbook) : Book由于 Book 和 AcademicWork 中都含有字段 author,
结构项构造子 Textbook.mk 不会将两个父结构类型下的项作为实参。它的类型签名为:
#check Textbook.mk
-- Textbook.mk (toBook : Book) (discipline : String) : Textbook
-- 在字段解析顺序中,Textbook > Book > AcademicWork结构项父投影函数包含 Textbook.toBook:
#check Textbook.toBook
-- Textbook.toBook (self : Textbook) : Book
#print Textbook.toBook
-- @[reducible] def Textbook.toBook
-- : Textbook → Book
-- :=fun self ↦ self.1然后是 Textbook.toAcademicWork:
其会将 Book 下面的 author 字段
以及未打包的 Discipline 字段结合起来。
#check Textbook.toAcademicWork
-- Textbook.toAcademicWork (self : Textbook) : AcademicWork
#print Textbook.toAcademicWork
-- @[reducible] def Textbook.toAcademicWork
-- : Textbook → AcademicWork
-- :=fun self ↦ {
-- author := self.author,
-- discipline := self.discipline
-- }对于新结构,旧结构的项投影函数可以直接拿来使用, 就好像新结构的所有字段是父结构字段的并集一样。 Lean 的阐述器会在字段使用时自动生成适当的项投影函数;同样地, 基于字段的 结构项构造以及 结构项更新 语法会隐藏继承编码的细节;然而 这些编码细节在 结构项构造子的调用、 结构项的匿名构造、 结构项投影的索引表示 时是暴露的。
structure Pair (α : Type u) where
fst : α
snd : α
deriving Repr
structure Triple (α : Type u) extends Pair α where
thd : α
deriving Repr
def coords : Triple Nat := {
fst := 17,
snd := 2,
thd := 95
}对 coords 的第一个字段索引进行求值
会得到底层的 Pair 而非字段 fst 的内容:
#eval coords.1
-- {
-- fst := 17,
-- snd := 2
-- }阐述器会将 coords.fst 翻译为 coords.toPair.fst。
给定下述关于偶数、偶素数以及一个具体的偶素数的声明:
structure EvenNumber where
val : Nat
isEven : 2 ∣ val := by decide
structure EvenPrime extends EvenNumber where
notOne : val ≠ 1 := by decide
isPrime : ∀ n, n ≤ val → n ∣ val → n = 1 ∨ n = val
def two : EvenPrime where
val := 2
isPrime := by
intros
repeat' (cases ‹Nat.le _ _›)
all_goals omega
def printEven (num : EvenNumber) : IO Unit :=
IO.print num.val将 two 直接传递给 printEven 会产生类型错误:
#check printEven two
-- Application type mismatch: The argument
-- two
-- has type
-- EvenPrime
-- but is expected to have type
-- EvenNumber
-- in the application
-- printEven two这是因为 EvenPrime 类型下的项
并不构成 EvenNumber 类型下的项。
#print 命令可以显示结构最重要的信息,包括
结构项父投影函数、
结构项构造子、
所有结构字段及其默认值以及
各个结构字段的解析顺序。
这些信息在处理诸如菱形继承等复杂继承关系时会非常有用。
下述代码展示了各种自行车的模型,包括
电动自行车、
非电动自行车以及
普通尺寸和大型家庭自行车。
最后一个结构类型 ElectricFamilyBike
在其继承关系图中包含一个菱形结构,
因为 FamilyBike 和 ElectricBike 都继承自 Bicycle。
structure Vehicle where
wheels : Nat
structure Bicycle extends Vehicle where
wheels := 2
structure ElectricVehicle extends Vehicle where
batteries : Nat := 1
structure FamilyBike extends Bicycle where
wheels := 3
structure ElectricBike extends Bicycle, ElectricVehicle
structure ElectricFamilyBike extends FamilyBike, ElectricBike where
batteries := 2#print 命令可以显示结构类型的重要信息:
#print ElectricBike
-- structure ElectricBike : Type
-- number of parameters: 0
-- parents:
-- ElectricBike.toBicycle : Bicycle
-- ElectricBike.toElectricVehicle : ElectricVehicle
-- fields:
-- Vehicle.wheels : Nat :=
-- 2
-- ElectricVehicle.batteries : Nat :=
-- 1
-- constructor:
-- ElectricBike.mk (toBicycle : Bicycle) (batteries : Nat) : ElectricBike
-- field notation resolution order:
-- ElectricBike, Bicycle, ElectricVehicle, Vehicle一个 ElectricFamilyBike 有三个轮子,
因为 FamilyBike 在其解析顺序中
排在 Bicycle 之前。
#print ElectricFamilyBike
-- structure ElectricFamilyBike : Type
-- number of parameters: 0
-- parents:
-- ElectricFamilyBike.toFamilyBike : FamilyBike
-- ElectricFamilyBike.toElectricBike : ElectricBike
-- fields:
-- Vehicle.wheels : Nat :=
-- 3
-- ElectricVehicle.batteries : Nat :=
-- 2
-- constructor:
-- ElectricFamilyBike.mk (toFamilyBike : FamilyBike) (batteries : Nat) : ElectricFamilyBike
-- field notation resolution order:
-- ElectricFamilyBike, FamilyBike, ElectricBike, Bicycle, ElectricVehicle, Vehicle4.4.3. 逻辑模型 Logical Model
4.4.3.1. 归纳项的递归子 Recursors
每个归纳类型都会有与之对应的归纳项递归子。
归纳项递归子由
归纳类型构造子的类型签名以及
归纳项构造子的类型签名
所完全决定。
归纳项递归子是一个函数,但是它们
是原始的,并且
不能通过 fun 来构造。
4.4.3.1.1. 归纳项递归子的类型 Recursor Types
归纳项递归子接收下述形参:
由于归纳类型构造子形参在调用时必须始终保持一致, 因此它们可以挪用进来,作为归纳项递归子的形参。
归纳项的递归子的动机 Motive
动机用于确定归纳项递归子被应用后所返回的值的类型。 动机是一个函数,其形参包含了 归纳类型构造子的索引以及一个索引全都被实例化的 归纳项。 至于动机所决定的类型,其所处的宇宙层级取决于 归纳类型的宇宙层级以及 具体的归纳项构造子。 具体请参阅子单例命题消去小节。
归纳项的递归子的小前提 Minor Premise,分别对应于每个归纳项构造子
对于每一个归纳项构造子,归纳项递归子都需要接收一个函数: 对于归纳项构造子的任何应用,这个函数都必须能够满足动机。 每一个小前提都会挪用归纳项构造子的所有形参。此外 如果某个归纳项构造子的某个形参的类型就是该归纳类型本身, 那么与该归纳项构造子对应的小前提函数就得再补充一个形参, 这个形参的类型是将“动机”应用到原参数值上的结果 —— 这样便可以接收对该形参对应的实参进行递归处理后所得到的结果。
归纳项的递归子的大前提 Major Premise,或称目标 Target
最后,归纳项递归子会接收该类型下的一个实例以及所有的索引值作为实参。
至于该归纳项递归子的最终结果类型, 其实就是将“动机”应用到这些索引和大前提上所得到的结果。
归纳类型构造子 Bool 对应的归纳项递归子 Bool.rec 拥有如下形参:
一个项递归子的动机
motive,其会根据一个给定的Bool归纳项计算出一个类型。两个项递归子小前提,分别对应于两个项构造子:
false,意味着动机可以被Bool.false所满足。true,意味着动机可以被Bool.true所满足。
一个项递归子大前提
t,类型为Bool。
inductive Bool : Type where
| Bool.false : Bool
| Bool.true : Bool
#check Bool.rec
-- Bool.rec.{u}
-- 项递归子的动机 {motive : Bool → Sort u}
-- 项递归子小前提 (false : motive false)
-- 项递归子小前提 (true : motive true)
-- 项递归子大前提 (t : Bool) :
-- 项递归子返回类型 motive t归纳类型构造子 List 对应的归纳项递归子 List.rec 拥有如下形参:
一个类型构造子索引
α。其之所以会出现是因为 动机、小前提以及大前提都需要引用它。一个项递归子的动机
motive,其会根据给定的List α下的归纳项计算出一个类型。 宇宙层级u和v之间没有联系。两个项递归子小前提:
nil意味着动机可以被List.nil所满足。cons意味着动机可以被List.cons head tail所满足——前提是 动机可以被tail所满足。注意到其中一个形参的类型为motive tail, 这是因为tail的类型里有List的递归出现。
一个项递归子大前提
t,类型为List α。
同样地,返回的类型就是将动机应用到大前提 t 上所得到的结果。
inductive List.{u} : Type u → Type u where
| List.nil : {α : Type u} → List α
| List.cons : {α : Type u} → α → List α → List α
#check List.rec
-- List.rec.{u_1, u}
-- 类型构造子索引 {α : Type u}
-- 项递归子的动机 {motive : List α → Sort u_1}
-- 项递归子小前提 (nil : motive [])
-- 项递归子小前提 (cons : (head : α) → (tail : List α) → motive tail → motive (head :: tail)) (t : List α) :
-- 项递归子返回类型 motive t归纳项递归子 EvenOddList.rec 与 List 的归纳项递归子非常相似,
只不过前者对应的类型构造子带有一个类型为 Bool 的索引。其形参如下:
一个项递归子的动机
motive。在这里索引也会变成动机的一个形参。两个项递归子小前提,分别对应于两个归纳项构造子:
nil,注意其类型有用到索引值true。cons,在这里索引会变成cons的一个形参。除此之外cons还会接收一个额外的形参,其类型为动机应用到tail上后的结果。
一个项递归子大前提
t,这里索引也会变成其的一个形参。
inductive EvenOddList (α : Type u) : Bool → Type u where
| nil : EvenOddList α true
| cons : α → EvenOddList α isEven → EvenOddList α (not isEven)
#print EvenOddList.rec
-- private recursor EvenOddList.rec.{u_1, u} :
-- 类型构造子实参 {α : Type u} →
-- 项递归子的动机 {motive : (a : Bool) → EvenOddList α a → Sort u_1} →
-- 项递归子小前提 (nil : motive true EvenOddList.nil) →
-- 项递归子小前提 (cons : {isEven : Bool} → (a : α) → (a_1 : EvenOddList α isEven) → motive isEven a_1 → motive (!isEven) (EvenOddList.cons a a_1)) →
-- 类型构造子索引 {a : Bool} →
-- 项递归子大前提 (t : EvenOddList α a) →
-- 项递归子返回结果 motive a t
--
-- 形参的个数 : 1 , 即 α : Type u
-- 索引的个数 : 1 , 即 a : Bool
-- 动机的个数 : 1 , 即 motive
-- 小前提个数 : 2 , 即 nil' , cons'
--
-- 归约的规则 :
-- 对于 EvenOddList.nil (0 字段) : λ α motive nil cons ↦ nil
-- 对于 EvenOddList.cons (3 字段) : λ α motive nil cons {isEven} a a_1 ↦ cons a a_1 (EvenOddList.rec nil cons a_1)如果动机是个谓词(即陪域为 Prop 的函数),
那么归纳项递归子就相当于归纳法:此时
对于非递归的归纳项构造子,它们对应的小前提就是基本情况;而对于那些
接收递归实参的归纳项构造子,小前提中需要额外补充的形参就是归纳假设。
4.4.3.1.1.1. 子单例命题消去 Subsingleton Elimination
Lean 里面的证明都是计算无关 Computationally Irrelevant 的。换句话说,
程序在被提供了一个命题的某些证明后是无法检查自己究竟收到了哪一个具体的证明的。
这一点在归纳命题(构造子)对应的归纳项递归子的类型签名中皆有体现:对于这些类型,
如果某个定理存在多个潜在的证明,
那么动机无论如何也只能返回另一个 Prop 下的项;但
如果类型的结构使其无论如何都只有至多一个证明,
那么动机就可以返回任何宇宙中的类型。
一个只有至多一个居留元的命题称作是子单例命题 Subsingleton。
为了不强制要求用户证明最多只有一个可能的证明,
一个保守的语法被用来近似检查某个命题是否是子单例的。
满足下列两个要求的命题会被认为是子单例的:
最多只有一个归纳项构造子。
每个项构造子的形参类型 要么是
Prop, 要么是一个形参,不然 就是一个索引。
True 是一个子单例命题,因为
它只有一个归纳项构造子,并且
这个归纳项构造子没有任何形参。
对应的归纳项递归子类型签名如下:
inductive True : Prop where
| intro
#check True.rec
-- True.rec.{u}
-- 项构造子的动机 {motive : True → Sort u}
-- 项递归子小前提 (intro : motive True.intro)
-- 项递归子大前提 (t : True) :
-- 项递归子返回类型 motive tFalse 是一个子单例命题,因为
它没有归纳项构造子。它的归纳项递归子具有如下类型签名:
inductive False : Prop where
#check False.rec
-- False.rec.{u}
-- 项递归子的动机 (motive : False → Sort u)
-- 项递归子大前提 (t : False) :
-- 项递归子返回类型 motive t注意到动机是显式形参,这是因为 它没有被任何后续的小前提或大前提所提及, 因此无法通过合一来解决。
And 是一个子单例命题构造子,因为它只有一个归纳项构造子,并且
这个构造子的两个形参的类型都是 Prop。它的归纳项递归子具有如下类型签名:
structure And (a b : Prop) : Prop where
left : a
right : b
#check And.rec
-- And.rec.{u}
-- 项递归子的动机 {motive : a ∧ b → Sort u}
-- 项递归子小前提 (intro : (left : a) → (right : b) → motive (And.intro left right))
-- 项递归子大前提 (t : a ∧ b) :
-- 项递归子返回类型 motive tOr 不是一个子单例命题构造子,因为它有两个归纳项构造子。
它的归纳项递归子具有如下类型签名:
inductive Or : Prop → Prop → Prop where
| Or.inl : ∀ {a b : Prop}, a → a ∨ b
| Or.inr : ∀ {a b : Prop}, b → a ∨ b
#check Or.rec
-- Or.rec.{u}
-- 项递归子的动机 {motive : a ∨ b → Sort u}
-- 项递归子小前提 (inl : ∀ (h : a), motive (.inl h))
-- 项递归子小前提 (inr : ∀ (h : b), motive (.inr h))
-- 项递归子大前提 (t : a ∨ b) :
-- 项递归子返回类型 motive t动机的类型意味着 Or.rec 只能被用于产生证明。
析取证明项可以被用于证明其他命题,
但是程序无法检查两个命题中究竟是哪个为真并且被用于证明。
Eq 是一个子单例命题构造子,因为它只有一个归纳项构造子,即 Eq.refl。
这个构造子会将 Eq 的索引实例化为一个形参值,
如此所有的参数都会是类型构造子的形参:
Eq.refl.{u} {α : Sort u} (x : α) : Eq x x对应的归纳项递归子具有如下类型签名:
inductive Eq.{u_1} {α : Sort u_1} (a : α) : α → Prop where
| refl : Eq a a
#check Eq.rec
-- Eq.rec.{u, v}
-- 类型构造子形参 {α : Sort v}
-- 类型构造子形参 {x : α} -- 对应 (a : α) 里面的 a
-- 项递归子的动机 {motive : (y : α) → x = y → Sort u}
-- 项递归子小前提 (refl : motive x (.refl x))
-- 类型构造子索引 {y : α} -- 对应 α → Prop 里面的 α
-- 项递归子大前提 (t : x = y) :
-- 项递归子返回类型 motive y t 这意味着相等关系的证明可以被用于重写非命题类型。
4.4.3.1.2. ι 归约 Reduction
除了可以向逻辑中添加新的常量之外,归纳声明还会添加新的归约规则: 这些规则支配着归纳项递归子与归纳项构造子之间的交互;具体来说即 归纳项递归子的大前提是一个归纳项构造子。这种形式的归约被称作是 ι 归约。
如果归纳项递归子的大前提只是一个归纳项构造子(其没有接收任何递归实参), 那么归纳项递归子的应用就会被归约为对归纳项构造子的小前提的应用;反之 如果递归实参存在, 那么 Lean 会将归纳项递归子应用到递归实参上以可获得小前提所需的实参。
4.4.3.2. 形式良好性的要求 Well-Formedness Requirements
归纳类型构造子声明受到一系列形式良好性要求的约束。这些要求能够确保 Lean 在加入新的归纳规则后依然能够保持逻辑一致性。这些要求是保守的: 尽管存在潜在的不会破坏一致性的归纳声明,但它们仍可能被这些要求拒绝。
4.4.3.2.1. 宇宙层级 Universe Levels
归纳类型构造子要么
居留于某个宇宙中,不然就是
居留于一个函数类型中——此时构造子的值域位于某个宇宙中。
每个归纳项构造子必须居留于
一个函数类型中,其值域是归纳类型构造子的饱和应用。
如果归纳类型所处宇宙为 Prop,
那么对于宇宙就没有进一步的限制,
因为 Prop 具有非直谓性;而
如果归纳类型所处宇宙非 Prop,
那么归纳项构造子的每个形参必须满足下述要求:
如果项构造子的形参是类型构造子的形参或索引, 那么这个形参的类型就不能超出类型构造子所处宇宙的范围。
其他项构造子的形参都必须小于类型构造子所处宇宙的范围。
Either 所处宇宙是其两个形参所处宇宙的最大者,
因为形参 α 和 β 都是归纳类型构造子的形参:
inductive Either (α : Type u) (β : Type v) : Type (max u v) where
| inl : α → Either α β
| inr : β → Either α βCanRepr 所处宇宙比其项构造子的形参 α 所处宇宙要大,
因为 α 不是归纳类型构造子的形参:
inductive CanRepr : Type (u + 1) where
| mk : (α : Type u) → [Repr α] → CanRepr没有项构造子的归纳类型构造子可以居留于比其形参所处宇宙更小的宇宙中:
inductive Spurious (α : Type 5) : Type 0 where但是这样可能导致无法在不改变其层级的情况下为 Spurious添加项构造子。
4.4.3.2.2. 严格正性 Strict Positivity
归纳类型构造子在对应的 归纳项构造子的形参的类型中的所有出现 都必须处于严格正性 Strictly Positive 的位置。一个位置是严格正性的,意味着 它既不位于函数的形参类型中(无论它被多少层函数类型包裹),并且 它也不构成任何表达式的实参 —— 除非表达式是归纳类型构造子。 这个限制可以剔除掉不安全的归纳声明, 但同时也剔除掉了一些没有问题的声明。
类型 Bad 的声明会被拒绝,否则它破坏 Lean 的逻辑一致性:
inductive Bad where
| bad : (Bad → Bad) → Bad
-- (kernel) arg #1 of 'Bad.bad' has a non positive occurrence
-- of the datatypes being declared这是因为它可能会在基于假设 Bad 的情况下写出一个循环论证来证明 False。
归纳项构造子 Bad.bad 之所以被拒绝是因为其定义域为 Bad → Bad:
这是一个函数类型,并且 Bad 出现在了其定义域中。
下面这个 Y 不动点组合子运算符的声明也会被拒绝,
因为 Fix 出现在了 f 的实参当中:
inductive Fix (f : Type u → Type u) where
| fix : f (Fix f) → Fix f
-- (kernel) arg #2 of 'Fix.fix' contains a non valid occurrence
-- of the datatypes being declaredFix.fix 被拒绝的原因如下:
尽管 f 不是归纳类型构造子,
但是 Fix 本身却出现在了 f 的实参当中。在这种情况下,
Fix 也足以构造出一个与 Bad 等价的类型:
def Bad : Type := Fix fun t => t → t4.4.3.2.3. 命题 vs 数据类型 Prop vs Type
Lean 会拒绝掉那些无法在实践中以多态方式使用的宇宙多态类型构造子的声明。
这种情况可能会发生在当实例化某些宇宙层级形参会导致最终的类型变成 Prop 的时候。
假如这个类型不是子单例的,那么它对应的归纳项递归子就只能针对命题(意味着动机必须返回一个 Prop)。
这些类型只有在作为 Prop 本身才有意义,因此宇宙多态性可能是一个错误。
由于它们在很大程度上是无用的,因此 Lean 归纳类型阐述器在设计时没有考虑支持这些类型。
如果这样的宇宙多态的归纳类型确实是子单例,那么它们的声明就是有意义的。
Lean 的标准库定义了 PUnit 和 PEmpty。
若想定义一个可以居留在 Prop 或 Type 中的子单例,
请将选项 bootstrap.inductiveCheckResultingUniverse 设置为 false。
bootstrap.inductiveCheckResultingUniverse默认值:true。
默认情况下,
如果结果宇宙的层级不是零,
但是对于某些宇宙层级形参而言可能为零,
那么 inductive / structure 命令就会报错。原因如下:
除非这个类型是子单例,否则它几乎不可能是用户想要的,因为它只能消去变成 Prop。
在 Init 包中,我们定义了子单例,并且我们使用这个选项来禁用检查。
这个选项可能会在我们改进验证器之后被删除。
不允许声明可位于任意宇宙中的 Bool:
inductive PBool : Sort u where
| true
| false
-- Invalid universe polymorphic resulting type:
-- The resulting universe is not `Prop`,
-- but it may be `Prop` for some parameter values:
-- Sort u
-- Hint: A possible solution is to use levels of the form `max 1 _` or `_ + 1` to -- ensure the universe is of the form `Type _`4.4.3.3. 一些用于可以停机性检查的常量 Constructions for Termination Checking
除了为归纳声明创建了 类型构造子、 项构造子、 项递归子以外, Lean 的核心类型理论还构造了一些有用的辅助工具。 首先,方程编译器(其会将带有模式匹配的递归函数翻译为项递归子的应用)会借用下述额外构造:
recOn是项递归子的一种版本,其中 项递归子的大前提和类型构造子的索引位于所有小前提之前。casesOn是项递归子的一种版本,其中 项递归子的大前提和类型构造子的索引位于所有小前提之前,并且 递归实参不会产生归纳假设。其相当于是 分类讨论而非原始递归。below会计算出一个类型,这个类型对于某些动机来说意味着 所有构成大前提子树的归纳类型下的居留元都能满足该动机。它把 用于归纳或原始递归的动机转换为 用于强递归或强归纳的动机。brecOn是项递归子的一种版本,其中below不仅被用于提供对直接递归形参 immediate recursive parameters TODO 的访问, 还会被用于提供对所有子树的访问。它相当于是强归纳。noConfusion是一个通用的陈述,从中可推断出 项构造子的单射性 Injectivity 和不交性 Disjointness。noConfusionType是一个Eq.ndrec的动机,用于noConfusion;它决定了 两个项构造子相等所蕴含的结论: 如果两个项构造子不同,那么结论直接就是False; 如果两个项构造子相同,那么结论就是它们对应的各个实参彼此相等。
这些常量遵循 McBride、Goguen 和 McKinna (2004) 中的描述。
Conor McBride, Healfdene Goguen, and James McKinna, 2004. “A Few Constructions on Constructors”. In Types for Proofs and Programs, International Workshop, TYPES 2004. (LNCS 3839).
对于基数良好的递归,
通常还需要有一个通用意义上的“大小”概念。SizeOf 类型类此时便派上了用场:
SizeOfSizeOf 是一个类型类,会为每个归纳声明自动派生;
它为归纳声明提供了一个到 Nat 的大小函数。
默认实例会令每个项构造子的“大小”等于 1 再加上所有项构造子字段的“大小”之和。
这个机制通常被用于基数良好的归纳声明;这是因为
项构造子的每个字段的“大小”都比项构造子本身更小,并且在许多情况下
这就足以证明一个递归函数只会被应用在更小的值上。
如果默认的证明策略失败,
那么这边建议在声明函数常量时使用 termination_by 子句来自定义大小度量。
类型类实例构造子
SizeOf.mk.{u}
类型类实例方法
sizeOf : α → Nat
一个归纳类型下元素的“大小”是一个自然数,其在每个归纳字段上会递减。
4.4.4. 运行时表示 Run-time Representation
An inductive type’s run-time representation depends both on how many constructors it has, how many arguments each constructor takes, and whether these arguments are relevant.
4.4.4.1. Exceptions
Not every inductive type is represented as indicated here — some inductive types have special support from the Lean compiler:
The representation of the fixed-width integer types
UInt8, …,UInt64,Int8, …,Int64, andUSizedepends on whether the code is compiled for a 32- or 64-bit architecture. Their representation is described in a dedicated section.Charis represented byuint32_t. BecauseCharvalues never require more than 21 bits, they are always unboxed.Floatis represented by a pointer to a Lean object that contains a “double”.An enum inductive type of at least $2$ and at most $2^{32}$ constructors, each of which has no parameters, is represented by the first type of
uint8_t,uint16_t,uint32_tthat is sufficient to assign a unique value to each constructor. For example, the typeBoolis represented byuint8_t, with values0forfalseand1fortrue.Decidable αis represented the same way asBoolNatandIntare represented bylean_object *. Their representations are described in more detail in the section on natural numbers and the section on integers.
4.4.4.2. Relevance
Types and proofs have no run-time representation. That is,
if an inductive type is a Prop,
then its values are erased prior to compilation. Similarly,
all theorem statements and types are erased.
Types with run-time representations are called relevant,
while types without run-time representations are called irrelevant.
Even though List.cons has the following signature,
which indicates three parameters:
List.cons.{u} {α : Type u} : α → List α → List αits run-time representation has only two, because the type argument is run-time irrelevant.
Even though Fin.mk has the following signature,
which indicates three parameters:
Fin.mk {n : Nat} (val : Nat) : val < n → Fin nits run-time representation has only two, because the proof is erased.
In most cases, irrelevant values simply disappear from compiled code. However, in cases where some representation is required (such as when they are arguments to polymorphic constructors), they are represented by a trivial value.
4.4.4.3. Trivial Wrappers
An inductive type is a trivial wrapper if it has has exactly one constructor and that constructor has exactly one run-time relevant parameter. Trivial wrappers are represented identically to their constructor’s parameter in the following circumstances:
The inductive type is private.
The type is public, and the public scope of the module in which it is defined contains enough information to determine that it is a trivial wrapper.
The type is defined in a source file that is not a module.
The structure Subtype bundles an element of some type with a proof that
it satisfies a predicate. Its constructor takes four arguments,
but three of them are irrelevant:
Subtype.mk.{u} {α : Sort u} {p : α → Prop}
(val : α) (property : p val) : Subtype pThus, subtypes impose no runtime overhead in compiled code,
and are represented identically to the type of the val field.
Int8, …, Int64, ISize are structures
with a single field that wraps the corresponding unsigned integer type.
They are represented by the unsigned C types
uint8_t, …, uint64_t, size_t, respectively,
because they have a trivial structure.4.4.4.4. Other Inductive Types
If an inductive type doesn’t fall into one of the categories above, then its representation is determined by its constructors. Constructors without relevant parameters are represented by their index into the list of constructors, as unboxed unsigned machine integers (scalars). Constructors with relevant parameters are represented as an object with a header, the constructor’s index, an array of pointers to other objects, and then arrays of scalar fields sorted by their types. The header tracks the object’s reference count and other necessary bookkeeping.
Recursive functions are compiled as they are in most programming languages, rather than by using the inductive type’s recursor. Elaborating recursive functions to recursors serves to provide reliable termination evidence, not executable code.
4.4.4.4.1. FFI
4.4.5. 互为递归的类型 Mutual Inductive Types
索引 Index
术语
- 非依值函数类型 Non-Dependent Function(见 §4-1)
- 归纳类型 Inductive Type(见 §4-4)
- 归纳类型构造子 Type Constructor(见 §4-4)
- 归纳类型构造子的索引 Index(见 §4-4-1-1)
- 归纳类型构造子的形参 Parameter(见 §4-4-1-1)
- 归纳项的递归子 Recursor(见 §4-4)
- 归纳项的递归子的大前提 Major Premise(见 §4-4-3-1-1)
- 归纳项的递归子的动机 Motive(见 §4-4-3-1-1)
- 归纳项的递归子的目标 Target(见 §4-4-3-1-1)
- 归纳项的递归子的小前提 Minor Premise(见 §4-4-3-1-1)
- 归纳项的构造子 Constructor(见 §4-4)
- 归纳项构造子调用的匿名表示 Anonymous Constructor Syntax(见 §4-4-1-3)
- 函数定义域 Domain(见 §4-1)
- 函数类型 Function Type(见 §4-1)
- 函数类型的内涵性 Function Intensionality(见 §4-1-3)
- 函数类型有外延性 Function Extensionality(见 §4-1-3)
- 函数陪域 Codomain(见 §4-1)
- 函数项的柯里化 Currying(见 §4-1-2)
- 函数项的完全性 Totality(见 §4-1-4)
- 结构项父投影函数 Parent Projection(见 §4-4-2-3)
- 结构项构造子调用的实例表示 Instance Notation(见 §4-4-2-2)
- 结构项投影函数 Projection(见 §4-4-2)
- 结构字段 Field(见 §4-4-2-1)
- 结构字段名称 Field Name(见 §4-4-2-2)
- 结构字段缩写 Field Abbreviation(见 §4-4-2-2)
- 结构字段索引 Field Index(见 §4-4-2-2)
- 命题类型 Proposition Type(见 §4-2)
- 命题类型的外延性 Extensionality(见 §4-2)
- 命题类型的证明无关性 Proof Irrelevance(见 §4)
- 命题类型有非直谓性 Impredicativity(见 §4-3-1)
- 模式化定义 Schematic Definition(见 §4-3-2)
- 商类型归约 Quotient Reduction(见 §4)
- 项的 β 归约 β-Reduction(见 §4)
- 项的 δ 归约 δ-Reduction(见 §4)
- 项的 ζ 归约 ζ-Reduction(见 §4)
- 项的 η 等价 η-Equivalence(见 §4)
- 项的 ι 归约 ι-Reduction(见 §4)
- 项的定义等价性 Definitional Equality(见 §4)
- 项的归约 Reduction(见 §4)
- 项的规范形式 Normal Form(见 §4)
- 项是类型良好的 Well-Typed(见 §4)
- 依值函数类型 Dependent Function(见 §4-1)又称依值积类型
- 依值积类型 Dependent Product(见 §4-1)
- 已声明常量 Declared Constant(见 §4)
- 宇宙 Universe(见 §4-3)
- 宇宙层级 Universe Level(见 §4-3)
- 宇宙层级形参 Universe Parameter(见 §4-3-2)
- 宇宙多态性 Universe Polymorphism(见 §4-3-2)
- 宇宙提升 Universe Lifting(见 §4-3-2-3)
- 直谓性 Predicativity(见 §4-3-1)
- 子单例命题 Subsingleton(见 §4-4-3-1-1-0)
语法
- 宇宙层级表达式的构造(见 §4-3-2-1)
- 宇宙层级变量声明(见 §4-3-2-2)
universe … - 归纳类型声明(见 §4-4-1)
inductive … where (| … )* (deriving …)? - 归纳项的构造(匿名表示)(见 §4-4-1-3)
⟨ … ⟩ - 结构类型声明(见 §4-4-2)
structure … (: …)? (extends (… : )?…)? where (… ::)? … (deriving …)? - 结构项的构造(实例表示)(见 §4-4-2-2)
{ … (: …)? }… := private? … - 结构项的更新(见 §4-4-2-2)
{… with … (: …)?}
常量
Function.comp(见 §4-1-5-1)函数项的复合运算Function.const(见 §4-1-5-1)常值函数项的构造Function.curry(见 §4-1-5-1)函数项的柯里化Function.uncurry(见 §4-1-5-1)函数项去柯里化Function.Injective(见 §4-1-5-2)函数项是单射Function.Surjective(见 §4-1-5-2)函数项是满射Function.LeftInverse(见 §4-1-5-2)函数项是左逆Function.HasLeftInverse(见 §4-1-5-2)函数项有左逆Function.RightInverse(见 §4-1-5-2)函数项是右逆Function.HasRightInverse(见 §4-1-5-2)函数项有右逆
定理
公理
归纳
结构
类型类
选项
inductive.autoPromoteIndices: 默认为true(见 §4-4-1-1)是否自动提升归纳类型构造子的索引为形参bootstrap.inductiveCheckResultingUniverse: 默认为true(见 §4-4-3-2-3)是否自动拒绝值域可能会变成Prop的归纳类型构造子声明
属性
例子
- 04-例子-01:
类型计算示范:
LengthList - 04-01-例子-01:
依值函数项的构造示范:
two - 04-01-例子-02:
依值函数类型与非依值函数类型上的 α 等价示范:
Nat → String,(n : Nat) → n + 1 = 1 + n - 04-01-例子-03:
非依值函数类型不会绑定形参名称示范:
AllNonZero - 04-01-例子-04:
函数类型构造时形参的显、隐式声明不影响定义等价性示范:
{α : Type} → (x : α) → α - 04-01-例子-05:
恐慌示范:
thirdChar - 04-03-例子-01:
命题类型有非直谓性示范:命题类型的证明无关性,
@Eq.refl.{0}、@Eq.refl.{5}的类型 - 04-03-例子-02:
函数类型所处宇宙的层级示范:
λ (α : Type 1) (β : Type 2) ↦ (α → β : Type 2) - 04-03-例子-03:
数据类型有直谓性示范:
λ (α : Type 1) (β : Type 2) ↦ (α → β : Type 1) - 04-03-例子-04:
宇宙有非累积性示范:
λ (α : Type 1) (β : Type 2) ↦ (α → β : Type 3) - 04-03-例子-05:
宇宙多态性在恒等函数中的体现示范:
id - 04-03-例子-06:
宇宙层级表达式的构造以及宇宙实参的自动推断示范:
Codec - 04-03-例子-07:
宇宙多态性可能会对定义等价性产生影响示范:
T - 04-03-例子-08:
宇宙单态性在隐式形参自动声明中的体现示范:
count - 04-03-例子-09:
宇宙层级形参的手动声明与调用示范:
List.map - 04-03-例子-10:
类型签名里未绑定的宇宙层级名称的自动绑定示范:
List.map - 04-03-例子-11:
宇宙层级变量声明示范:
@id - 04-03-例子-12:
宇宙层级变量声明示范:
L - 04-04-例子-01:
归纳项构造子完全没有的归纳类型示范:
Empty - 04-04-例子-02:
归纳项构造子完全没有的归纳命题示范:
False - 04-04-例子-03:
归纳项构造子仅有一个的归纳类型示范:
Unit - 04-04-例子-04:
归纳项构造子仅有一个的归纳命题示范:
True - 04-04-例子-05:
归纳类型构造子同时具有形参和索引示范:
EvenOddList - 04-04-例子-06:
归纳类型构造子和归纳项构造子的形参示范:
Sum - 04-04-例子-07:
归纳项构造子调用的匿名表示示范:
AtLeastOne,oneTwoThree - 04-04-例子-08:
结构项构造子的宇宙层级形参自动推导示范:
Prod - 04-04-例子-09:
结构字段的隐式形参自动声明示范:
MyStructure - 04-04-例子-10:
结构项投影函数依赖先前字段的值示范:
ArraySized - 04-04-例子-11:
结构字段默认值的设置示范:
Graph - 04-04-例子-12:
结构项构造子的重命名示范:
Palindrome - 04-04-例子-13:
结构项构造子声明的时候使用声明修饰器示范:
NatStringBimap - 04-04-例子-14:
结构项在结构字段有默认值时的模式匹配示范:
AugmentedIntList - 04-04-例子-15:
结构常量构造时字段值的私有化示范:
Complex,secret,x - 04-04-例子-16:
结构函数私有常量的调用示范:
State - 04-04-例子-17:
结构常量更新数组字段值里的元素示范:
AugmentedIntArray - 04-04-例子-18:
结构常量声明的实例表示可以省略花括号示范:
Prod,location - 04-04-例子-19:
结构继承对结构字段重叠问题的处理示范:
Textbook,Book,AcademicWork - 04-04-例子-20:
结构继承与结构项投影的字段索引表示示范:
Triple,Pair,coords - 04-04-例子-21:
结构继承不存在子类型关系示范:
EvenPrime,EvenNumber,two,printEven - 04-04-例子-22:
结构内容打印示范:
ElectricFamilyBike,ElectricBike,FamilyBike,Bicycle,ElectricVehicle,Vehicle - 04-04-例子-23:
归纳项递归子在归纳类型构造子没有形参时的样子示范:
Bool - 04-04-例子-24:
归纳项递归子在归纳类型构造子带有形参时的样子示范:
List - 04-04-例子-25:
归纳项递归子在归纳类型构造子带有索引时的样子示范:
EvenOddList - 04-04-例子-26:
子单例命题对应的归纳项递归子示范:
True - 04-04-例子-27:
子单例命题对应的归纳项递归子示范:
False - 04-04-例子-28:
子单例命题构造子对应的归纳项递归子示范:
And - 04-04-例子-29:
非子单例命题构造子对应的归纳项递归子示范:
Or - 04-04-例子-30:
子单例命题构造子对应的归纳项递归子示范:
Eq - 04-04-例子-31:
归纳声明需要遵循形式良好性对宇宙层级的要求示范:
Sum,CanRepr,Spurious - 04-04-例子-32:
归纳声明需要遵循形式良好性对严格正性的要求示范:
Bad,Fix - 04-04-例子-33:
宇宙多态滥用会被拒绝示范:
PBool - 04-04-例子-34: 类型是无关的
- 04-04-例子-35: 证明是无关的
- 04-04-例子-36: 零负载器类型
- 04-04-例子-37: 有符号整数
微信