翻译-Lean Language Reference-13-项

13.1. 标识符 Identifiers

语法:名称调用
$x:ident

标识符项是对某个名称的调用。

标识符的具体词法规定 在 Lean 的具体语法 一节中 有详细说明。

标识符也会出现在它们绑定名称时所处的语境,比如 let 和 fun; 然而这些绑定出现处本身并不能构成完整的项。标识符到名称的映射并不简单: 在模块任意位置里可能有多个命名空间被打开,其中 可能有多个区段变量,也 可能有局部绑定。 此外标识符也可能包含多个由点分隔的原子标识符;点 既用于分隔命名空间和它们的内容, 也用于分隔变量和字段或者是用字段表示法调用的函数。 这就造成了歧义,因为标识符 A.B.C.D.e.f 可能指代以下任意一种情况:

  • 命名空间 A.B.C.D.e 中的名称 f (比如 e 当中 where 块里定义的函数项)。

  • 常量 A.B.C.D.e 类型为 T,然后 作为实参被塞给函数项 T.f

  • 结构常量 A.B.C.D.e 的字段 f 的投影。

  • 结构常量 A 的一系列字段 B、C、D、e 的投影 B.C.D.e, 然后再作为实参塞给函数项 f,并以字段表示法书写函数项的应用。

  • 如果命名空间 Q 被打开了, 那么它可能是上述任何一种情况并以 Q 作为前缀; 比如命名空间 Q.A.B.C.D.e 中的名称 f。

上述列表只列出了一小部分情况。 任给一个标识符,阐述器都必须查明 该标识符究竟指代哪(几)个名称、 尾部的内容究竟是字段还是函数项应用的字段表示。 这就是名称的解析 Resolving。

某些全局环境中的声明在第一次被引用时会被惰性创建。 如果在解析标识符的过程中 既创建了这样的声明, 又产生了对它的引用, 那么该解析就可被称作名称的实现 Realizing。 名称解析和实现的规则是相同的,因此 尽管本小节仅提及名称解析, 但它也同样适用于名称实现。

名称解析受下述因素影响:

标识符的任何前缀都可以被解析为一组名称。 那些没被包含在解析过程中的后缀随后会被当作字段投影或按字段表示法处理。 较长前缀的解析优先级高于较短的前缀;也就是说, 标识符中被当作字段表示法处理的部分会尽可能地少。 标识符前缀可能是以下列表中的任何一项, 列表当中上方条目的优先级高于下方条目:

  1. 一个局部受缚变量,其名称完全等同于标识符前缀(包括宏作用域), 其中内层局部绑定名称的优先级高于外层局部绑定。

  2. 一个局部辅助声明, 其名称完全等同于标识符前缀。

  3. 一个区段变量, 其名称完全等同于标识符前缀。

  4. 一个全局名称,其名称要么 等同于将当前命名空间的前缀附加到标识符前缀上,要么就是 拥有一个别名,该别名位于当前命名空间的某个前缀当中(其中较长前缀的优先级高于较短前缀)。

  5. 一个全局名称, 其名称完全等同于标识符前缀,且 其通过 open 命令引入作用域。

如果标识符被解析为多个名称, 那么阐述器就会尝试使用所有名称。 如果恰好有一个成功, 那么它就会被用作该标识符的含义。 如果有多个成功或者全部失败, 那么就会报错。

13-01-例子-01:名称解析时内层局部声明优先

局部绑定优先级高于全局绑定:

def x := "global"
#eval
  let x := "local"
  x
  -- 前缀 x : 2 个标识符(分别是局部受缚变量、全局常量),优先考虑局部
-- "local"

最内层的局部绑定优先级高于其他绑定:

#eval
  let x := "outer"
  let x := "inner"
  x
  -- 前缀 x : 2 个标识符(都是两个局部受缚变量),优先考虑近的
-- "inner"

13-01-例子-02:名称解析时命名空间前缀更长的优先

命名空间 A、B、C 一层套一层。 命名空间 A、C 都包含一个 x 的声明:

namespace A
def x := "A.x"
namespace B
namespace C
def x := "A.B.C.x"

如果当前命名空间是 A.B.C, 那么 x 会被解析为 A.B.C.x。

#eval x
-- "A.B.C.x"
-- 前缀 x : 2 个标识符(位于命名空间 C 的前缀 A.B.C、A 中),优先考虑命名空间前缀更长的

如果当前命名空间是 A.B, 那么 x 会被解析为 A.x。

end C
#eval x
-- "A.x"

13-01-例子-03:名称解析时标识符前缀更长的优先

当标识符可能指代不同的来自名称的投影时, 前缀更长的那个优先级更高:

structure A where
  y : String
deriving Repr

structure B where
  y : A
deriving Repr

def y : B := ⟨⟨"shorter"⟩⟩
def y.y : A := ⟨"longer"⟩

给定上述声明,y.y.y 照理来说既可能 指代的是 y 的 y 字段的 y 字段(值为 "shorter"),也可能 指代的是 y.y 的 y 字段(值为 "longer"); 然而答案是后者,因为在所有 y.y.y 的前缀中 y.y 是更长的那个:

#eval y.y.y
-- "longer"
-- 前缀 y.y.y : 无标识符
-- 前缀 y.y : 1 个标识符(最后的 .y 为结构项投影),优先考虑标识符前缀更长的
-- 前缀 y : 1 个标识符,最后的 .y.y 为结构项投影

13-01-例子-04:名称解析时当前命名空间优先级高于已开启的命名空间

如果一个标识符指代的可能是 一个当前命名空间的前缀中定义的名称,或者是 一个已打开的命名空间中的名称, 那么前者优先级更高。

namespace A
def x := "A.x"
end A

namespace B
def x := "B.x"
namespace C
open A
#eval x
-- "B.x"
-- 前缀 x : 2 个标识符(分别位于当前命名空间 B.C 的前缀 B 以及已打开的命名空间 A),优先考虑当前命名空间的前缀

即便命名空间 A 的开启比常量 B.x 的声明更近, 标识符 x 仍然会被解析为 B.x 而不是 A.x; 这是因为 B 是当前命名空间 B.C 的前缀。

13-01-例子-05:名称解析失败

在本例中,x 既可能 指代 A.x,也可能 指代 B.x, 然而两者都没有优先级, 这是因为两者类型相同。 所以产生报错。

def A.x := "A.x"
def B.x := "B.x"
open A
open B
#eval x
-- Ambiguous term
--   x
-- Possible interpretations:
--   B.x : String  
--   A.x : String

13-01-例子-06:名称解析失败可以通过类型注解消除歧义

如果原本会产生歧义的名称拥有不同的类型, 那么系统会利用类型来消除歧义:

def C.x := "C.x"
def D.x := 3
open C
open D
#eval (x : String)
-- "C.x"

13.1.1. 先导 . Leading .

如果一个标识符是以点号(.)开头的, 那么在对该标识符进行名称解析时会借助 阐述器所预期的表达式的类型,而非借助 当前命名空间以及一系列已开启的命名空间。 函数项应用的字段表示与此相关: 在对标识符进行名称解析时,名称调用的先导点号表示 Leading Dot Notation 用的是标识符的预期类型,而 字段表示法则用的是紧跟在点前面的项所被推断出的类型。

以点号 . 开头的标识符会在与预期类型同名的命名空间中被查找, 如果项的预期类型形如一个常量被应用于零个或多个实参, 那么它的命名空间就是该常量的名称。 如果项的预期类型并非常量的应用(比如一个函数项、一个元变量或者一个宇宙), 那么它就没有命名空间。

如果标识符的名称无法在预期类型对应的命名空间中被找到, 但是该常量可以被进一步展开为另一个常量, 那么后者的命名空间就会被调查; 这个过程会被重复进行,直至 遇到某个不再是函数常量应用的东西,或者是 该常量无法再被继续展开 为止。

13-01-例子-07:名称调用的先导点号表示

.replicate 的类型的推导结果应该是 List Unit, 而该类型对应的命名空间是 List, 因此 .replicate 会被解析为 List.replicate。

#eval show List Unit from .replicate 3 ()
-- [(), (), ()]

13-01-例子-08:名称调用的先导点号表示以及名称的展开

.replicate 的类型的推导结果应该是 MyList Unit。 尽管该类型对应的命名空间是 MyList 并且 MyList.replicate 的定义并不存在。 但是将 MyList Unit 展开后得到 List Unit, 因此 .replicate 会被解析为 List.replicate。

def MyList α := List α
#eval show MyList Unit from .replicate 3 ()
-- [(), (), ()]

13.2. 函数类型 Function Types

Lean 的函数类型描述的不仅仅只有函数项的定义域和陪域, 它们还为函数项被应用处的阐述提供了指导:实参当中 一部分是通过 合一或者是类实例合成来自动发现的, 一部分则是带默认值的可选参数, 剩下的则应是通过自定义的策略脚本来合成的。 此外它们的语法还支持函数项柯里化的缩写。

语法:单参函数类型构造

依值函数类型包含一个显式名称:

term ::= ...
  | (ident : term) → term

非依值函数类型则没有:

term ::= ...
  | term → term

语法:多参函数类型构造

依值函数类型可能包含多个形参, 它们具有相同的类型,并且被包裹在一对圆括号内:

term ::= ...
  | (ident* : term) → term

这等同于在嵌套的函数类型中 重复为每个形参补充类型注解。

语法:函数类型构造(隐式、可选、自动形参声明)

函数类型可以描述那些类型中带有隐式、可选和自动形参的函数项。 除了实例隐式形参以外,函数项的其余形参都需要一个或多个名称。

term ::= ...
  | (ident* : term := term) → term
term ::= ...
  | (ident* : term := by tacticSeq) → term
term ::= ...
  | {ident* : term} → term
term ::= ...
  | [term] → term
term ::= ...
  | [ident : term] → term
term ::= ...
  | ⦃ident* : term⦄ → term

13-02-例子-01:函数类型构造(多个同类型形参声明)

Nat.add 的类型可以写成下述形式:

  • Nat → Nat → Nat

  • (a : Nat) → (b : Nat) → Nat

  • (a b : Nat) → Nat

三个写法都是定义等价的,只不过最后两种写法 允许函数项实参以具名实参的形式表示。

13.3. 函数项的构造 Functions

函数类型下的项可通过函数抽象语法来构造。 这是通过 fun 关键字来开启的。

函数抽象在其他社群中也被称作 lambda, 源于 Alonzo Church 为其创建的记号; 又或者被称作是匿名函数, 因为它们不需要在全局环境中被命名。

尽管函数项的构造在 Lean 的核心类型理论中只允许绑定单个形参名称, 但是在高层次的 Lean 语法中函数项的写法却相当灵活。

语法:单参函数项的构造

最基本的函数项构造语法会 绑定一个名称来 表示函数项的形参:

term ::= ...
  | fun ident => term

Lean 在阐述代码的时候必须能够确定函数项的定义域, 提供信息的其中一种方式是在形参声明时提供类型注解:

term ::= ...
  | fun ident : term => term

通过 def 声明的函数常量最后都会被脱糖变成 fun 的形式;此外, 归纳声明会引入函数类型下新的值(比如归纳类型构造子、归纳项构造子等), 而这些项本身无法仅由 fun 来构造。

语法:多参函数项的构造

fun 后面声明多个形参也是允许的:

term ::= ...
  | fun ident ident* => term
term ::= ...
  | fun ident ident* : term => term

多个形参的类型注解补充需要用上圆括号:

term ::= ...
  | fun (ident* : term) =>term

上述语法等价于多层嵌套的 fun 项。

本小节中提到的 => 都可以替换为 ↦。

构造函数项时也可以通过模式匹配语法来声明形参, 如此便不必再引入一个局部变量然后再对其进行模式匹配。 在模式匹配相关的小节中有详细介绍。

13.3.1. 隐式形参 Implicit Parameters

Lean 允许在构造函数项时声明隐式形参,这意味着 Lean 自己就可以为函数项提供实参,用户无需操心。 隐式形参有以下三种类别:

  • 普通隐式形参 Ordinary Implicit Parameters

    普通隐式形参需要 Lean 通过合一来确定对应实参的值。换句话说, 函数项的所有被调用处都应该有一个潜在的实参值与之对应, 如此使得完整的函数项调用是类型良好的。Lean 的阐述器会尽可能地 为函数项的每个隐式形参的所有出现找到对应的值。 普通隐式形参声明用花括号({ 和 })包裹。

  • 严格隐式形参 Strict Implicit Parameters

    严格隐式形参与普通隐式形参完全相同,唯一的区别是 只有在为函数项被调用处提供了后续的显式实参后 Lean 才会尝试去寻找该严格隐式形参对应实参的值。 严格隐式形参写在双花括号(⦃ 和 ⦄,或 {{ 和 }})中。

  • 实例隐式形参 Instance Implicit Parameters

    实例隐式形参对应的实参是通过类实例合成来寻找的。 实例隐式形参写在方括号([ 和 ])中。 与其他隐式形参不同的是,不带 : 的实例隐式形参声明指定的是 该形参的类型,而非 该形参的名称。 此外只允许为实例隐式形参指定单个名称。 另外大多数实例隐式形参声明都会直接省略形参名称, 这是因为被合成为函数项实参的实例 即便是没被显式命名在函数主体内也已经是直接可用的了。

13-03-例子-01:函数项构造时形参以普通隐式、严格隐式方式声明的区别

函数常量 f 与 g 之间的差别在于 形参 α 在 g 中是严格隐式的:

def f {α : Type} : α → α := fun x => x
def g ⦃α : Type⦄ : α → α := fun x => x

在被应用于具体实参时, 这些函数项的阐述过程是完全相同的:

example : f 2 = g 2 := rfl

使用 f 需要求解隐式的 α, 缺乏足够可用的信息会导致阐述失败:

example := f
-- don't know how to synthesize implicit argument `α`
--   @f ?m.3
-- context:
-- ⊢ Type

然而未提供显式实参时, 使用 g 并不需要求解隐式的 α:

example := g

语法:函数项的构造

更宽泛的 fun 语法允许接收一连串的括号绑定器 Function Binder funBinder:

term ::= ...
  | fun funBinder funBinder* => term

语法:函数项的构造(显、隐式形参声明、类型注解补充)

括号绑定器可以只是标识符:

funBinder ::= ...
  | ident

也可以是一连串标识符序列,包裹在圆括号内:

funBinder ::= ...
  | ([anonymous]ident ident*)

也可以是一连串标识符序列,后面跟着一个类型注解,然后一起包裹在圆括号内:

funBinder ::= ...
  | ([anonymous]ident ident* : term)

普通隐式形参,类型注解存在与否的情况:

funBinder ::= ...
  | {ident ident*}
funBinder ::= ...
  | {ident ident* : term}

严格隐式形参,类型注解存在与否的情况

funBinder ::= ...
  | ⦃ident ident*⦄
funBinder ::= ...
  | ⦃ident* : term⦄

实例隐式形参,匿名或具名的情况:

funBinder ::= ...
  | [term]
funBinder ::= ...
  | [ident : term]

和前面类似, _ 可以代替标识符来创建匿名形参, ⦃ 和 ⦄ 可以分别用 {{ 和 }} 代替。

Lean 的核心语言并不会区别对待普通隐式、实例隐式和显式形参: 各种不同的函数项和函数类型在底层都是定义等价的。 这种区别只有在阐述的过程中才能被观察到。

如果一个函数项的预期类型包含了隐式形参, 但是括号绑定器中却没有显示地写出这些隐式形参, 那么最终生成的函数带有的形参数量可能会多于代码绑定器中体现出的数量。 因为隐式形参可能会被自动添加进去。

13-03-例子-02:函数项构造时类型形参常以隐式方式声明

恒等函数项可以被声明为只有单个显式形参的形式。 只要知道了实参的类型,隐式形参就会被自动添加进去。

#check (fun x => x : {α : Type} → α → α)
-- fun {α} x => x : {α : Type} → α → α

下述代码都是等价的:

#check (fun {α} x => x : {α : Type} → α → α)
-- fun {α} x => x : {α : Type} → α → α
#check (fun {α} (x : α) => x : {α : Type} → α → α)
-- fun {α} x => x : {α : Type} → α → α
#check (fun {α : Type} (x : α) => x : {α : Type} → α → α)
-- fun {α} x => x : {α : Type} → α → α

13.4. 函数项的应用 Function Application

函数项的应用通常是通过并列的方式来书写的: 实参位于函数项后,两者间至少存在一个空格。 在 Lean 的类型理论中,所有函数项都会 接收一个实参并 产生一个唯一与该实参对应的值。 所有函数项的应用本质上都是单个函数与 单个实参的组合。 多个实参的情况则通过柯里化来表示。

面向用户的高层次项语言 会将函数与一个或多个实参视为一个统一的整体,并且还 支持如 隐式实参、 可选实参、 具名实参等附加功能,外加普通的 位置实参 阐述器会将这些东西转换为核心类型理论里更为简单的模型。

语法:函数项的应用

函数项的应用包含一个函数项,其后 跟着一个或多个实参,或是 跟着零个或多个实参外加一个省略号。

term ::= ...
  | term argument+
  | term argument* ..

语法:函数项的应用(位置、具名实参构造)

实参要么是 项,要么就是 具名实参。

argument ::= ...
  | term
  | ((ident | _:ident) :=term)

核心语言中的函数类型决定了实参在最终函数项应用表达式里的位置。 函数类型里包含了它们所期待的形参的名称。在 Lean 的核心语言中, 非依值函数类型也会被编码为依值函数类型,只不过对于后者而言 形参名称不会出现在函数类型的陪域中;此外 形参名称在内部会被选中,使其无法被写成具名实参的名称, 这对预防意外捕获至关重要。

函数项的每个形参都拥有一个名称。 通过递归遍历函数项实参的不同种类, Lean 会从实参列表中选中实参。方式如下:

  • 如果形参名称与具名实参的名称匹配, 那么该实参将被选中。

  • 如果形参是普通隐式的, 那么一个新的元变量将被创建并被选中,元变量类型为该形参的类型。

  • 如果形参是实例隐式的, 那么一个新的元变量将被创建并被插入,元变量类型为该形参的类型。 实例元变量稍后将会被合成。

  • 如果形参是严格隐式的, 并且还存在任何未被选中的具名实参或位置实参, 那么一个新的元变量将被创建并被选中,元变量类型为该形参的类型。

  • 如果形参是显式的,那么接下来的位置实参将被选中并被阐述。 如果没有位置实参的话,那么:

    • 如果形参为可选的, 那么其默认值将作为实参被选中。

    • 如果形参为自动的, 那么与之关联的策略脚本将被执行以构造实参。

    • 如果形参既不是可选的又不是自动的, 并且

      • 如果省略号没有, 那么一个新的变量将作为实参被选中。

      • 如果省略号存在, 那么一个新的元变量将被选中,同隐式形参的情形。

存在一个特殊情况:当 函数项的应用出现在模式中,并且 省略号存在时, 可选实参和自动实参 不会被插入, 而是会变成通用模式(_)。

如果函数项应用表达式的类型不是函数类型并且还有剩余实参, 那么会有报错; 如果所有实参都被插入完后还存在省略号, 那么缺失的实参就会被统统设置为新的元变量, 就好像这些实参是隐式的一样; 如果新变量名称是为了缺失的显式位置实参而绑定的, 那么整个函数项应用表达式就会被包裹在一个 fun 表达式中来绑定这些名称; 最后,类实例合成会被启用, 元变量会被尽可能多地求解:

  1. 完整的函数项应用表达式的类型会被推断出来。 这可能导致一些元变量被求解,因为类型推断过程中会发生合一。

  2. 类实例元变量会被合成。 只有在 被推断出的类型是个元变量,并且 该元变量是某个实例输出的参数时, 类默认实例才会被使用。

  3. 如果预期类型存在, 那么其会与被推断出的类型进行合一; 然而由此合一所产生的错误会被丢弃。 如果预期类型和被推断出的类型是相等的, 那么合一可以解决剩余的隐式实参元变量。 如果它们不相等,则不会抛出错误, 因为周围的阐述器可能可以插入类型强制转换或单子提升。

13-04-例子-01:函数项应用时位置实参和具名实参的混合使用

#check 命令可以用来检查函数项应用时所插入的实参。

函数常量 sum3 有三个类型为 Nat 的显式形参, 分别名为 x、y 和 z。

def sum3 (x y z : Nat) : Nat := x + y + z

三个实参可以写成位置实参:

#check sum3 4 3 8
-- sum3 4 3 8 : Nat

也可以写成具名实参的形式:

#check sum3 (x := 4) (y := 3) (z := 8)
-- sum3 4 3 8 : Nat

当实参以具名实参形式书写时, 它们的排列顺序可以是任意的。

#check sum3 (y := 3) (x := 4) (z := 8)
-- sum3 4 3 8 : Nat

具名实参和位置实参可随意混合使用。

#check sum3 (y := 3) 8 (x := 4)
-- sum3 4 3 8 : Nat

具名实参和位置实参可随意混合使用。 如果某个实参被写成了具名实参的形式, 那么即便它前面是一个可能被使用了的位置实参, 它也仍然会被选作实参。

#check sum3 8 (y := 3) (x := 4) 
-- sum3 4 3 8 : Nat

如果将具名实参插入到未被提供的位置实参后面, 那么一个新的函数项将会被创建,当中已提供的实参将会被填充。

#check sum3 (y := 3)
-- fun x ↦ sum3 x 3 : Nat → Nat → Nat
-- fun x ↦ sum3 x 3 : (x z : Nat) → Nat

形参的名称在背后其实会被保存在函数类型中, 这意味着剩余的实参也可以再次被写成具名实参的形式:

#check (sum3 (y := 3)) (x := 4)
-- (fun x ↦ sum3 x 3) 4 : Nat → Nat
-- (fun x ↦ sum3 x 3) 4 : (z : Nat) → Nat

尽管形参名称往往取自函数类型, 但是形参名称不必非得等同于函数类型中所使用的名称。 这意味着即便局部声明与形参名称发生冲突,也不会影响具名实参的使用; 因为形参名称在这里会被临时修改以避免冲突。函数类型中则仍保持不变。

在下面的例子中,原先被拿来命名 sum3 首个形参的 x 已经被重命名了,以避免与 let 里面的 x 发生冲突:

#check let x := 8 ; sum3 (z := x)
-- let x := 8;
-- fun x_1 y ↦ sum3 x_1 y x : Nat → Nat
-- fun x_1 y ↦ sum3 x_1 y x : (x y : Nat) → Nat

即便 x 被重命名为 x_1, 其仍然可以被用在具名实参中:

#check (let x := 8 ; sum3 (z := x)) (x := 4)
-- (let x := 8;
--  fun x_1 y ↦ sum3 x_1 y x) 4
-- : Nat → Nat
-- : (y : Nat) → Nat

这是因为名称 x 依然被用于函数类型中。 启用选项 pp.piBinderNames 可以显示函数类型中形参的名称:

set_option pp.piBinderNames true in
#check let x := 8 ; sum3 (z := x)
-- let x := 8;
-- fun x_1 y ↦ sum3 x_1 y x : Nat → Nat
-- fun x_1 y ↦ sum3 x_1 y x : (x y : Nat) → Nat

可选形参和自动形参 都不是 Lean 核心类型理论内的概念。 它们分别是通过 optParam 和 autoParam 两个零件所实现的:

常量:optParam
optParam.{u} (α : Sort u) (default : α) : Sort u

一个零件,用于实现可选形参。

声明中形如 (x : α := default) 的括号绑定器其实是 x : optParam α default 的语法糖,并且在实参未被提供的时候会 触发阐述器尝试使用 default 作为实参。

常量:autoParam
autoParam.{u} (α : Sort u) (tactic : Lean.Syntax) : Sort u

一个零件,用于实现自动形参。

与 optParam 类似,只不过其使用了给定的策略脚本。 这个零件与 optParam 一样只会影响阐述过程。比如: 策略脚本不会在类实例合成过程中被运行。

13.4.1. 广义字段表示法 Generalized Field Notation

结构字段相关的小节里 提到了结构项投影的一种简便写法,即字段表示法。 与字段表示法类似,广义字段表示法通常形如 一个项后面跟着 一个点(.)再加 一个标识符。 三者之间没有空格。

语法:函数项的应用(字段表示)
term ::= ...
  | term.ident

如果一个项的类型形如某个常量被应用于零或多个实参上, 那么无论该项是结构项还是类实例(它们都拥有字段), 其作为实参传入给某个函数项的过程都可以写成字段形式。像这种将 非结构项投影函数的应用写成形如结构项投影的做法我们称作是函数项应用的广义字段表示 Generalized Field Notation。

点后面的标识符会在项类型对应的命名空间中进行查找,命名空间的名称即类型常量的名称。 如果类型并不是形如某个常量的应用(如元变量或宇宙),那么它就没有命名空间,广义字段表示法也就无法使用。 作为一种特殊情况,如果表达式是一个函数项,那么广义字段表示法就会在 Function 命名空间中进行查找; 如此 Nat.add.uncurry 其实就相当于 Function.uncurry Nat.add。

如果字段没有被找到但是常量可以被展开并产生一个类型, 那么这套分析过程就会再针对新类型继续执行。

当一个函数项被读取到时, 点前面的项会变成函数项的一个实参。具体点说, 它会变成函数项的首个类型匹配成功的显式实参。 此后函数项应用的阐述过程就和普通情况一样了。

13-04-例子-02:函数项应用的字段表示以及类型常量的展开

类型 Username 是一个常量, 因此 Username 命名空间中的函数项 应用于 Username 下的项的过程 就可以通过广义字段表示法来表示

def Username := String

我们可以拿 Username.validate 来做示范: 其用于检查 用户名的头部是否为空格,以及 用户名是否只用到允许使用的一小部分字符。 在其定义中,广义字段表示法 被用来调用函数常量 String.isPrefixOf、 String.any、 Char.isAlpha 和 Char.isDigit。 String.isPrefixOf 接收两个类型为 String 的实参, " " 被选为第一个实参,因为它是点前面的项。 String.any 在 name 上的应用可以通过广义字段表示法来表示, 即便 name 的类型为 Username;这是 因为 Username.any 并没有被定义, 并且 Username 展开后会变成 String。

def Username.validate (name : Username) : Except String Unit := do
  if " ".isPrefixOf name then
    throw "Unexpected leading whitespace"
  if name.any notOk then
    throw "Unexpected character"
  return ()
where
  notOk (c : Char) : Bool :=
    !c.isAlpha &&
    !c.isDigit &&
    !c ∈ ['_', ' ']

def adminUser : Username := "admin"

但是 Username.validate 在 "admin" 上的应用不能通过广义字段表示法来表示, 因为 String 无法被展开为 Username。

#eval "admin".validate
-- Invalid field `validate`: The environment does not contain 
-- `String.validate`, so it is not possible to 
-- project the field `validate` from an expression
--   "admin"
-- of type `String`

另一方面,由于 adminUser 的类型是 Username, 因此 Username.validate 函数项就可以通过广义字段表示法来应用:

#eval adminUser.validate
-- Except.ok ()

反过来,String.any 在 adminUser : Username 上的应用 就可以通过广义字段表示法来表示:

#eval adminUser.any (· == 'm')
-- true

选项:pp.fieldNotation

默认值:true。

(美观打印器)在美观打印的时候会将函数项的应用以字段表示法书写(结构投影也会), 除非属性 pp_nodot 被启用。

属性:pp_nodot
attr ::= ...
  | pp_nodot

该属性可以让 Lean 的美观打印器 在打印函数项应用的时候不使用字段表示法。

13-04-例子-03:函数项应用的打印在字段表示启用、禁用时的区别

Nat.half 默认以字段表示法打印:

def Nat.half : Nat → Nat
  | 0 | 1 => 0
  | n + 2 => n.half + 1
#check Nat.half Nat.zero
-- Nat.zero.half : Nat

将 pp_nodot 添加在 Nat.half 上 会导致项以普通函数项应用语法显示。

attribute [pp_nodot] Nat.half
#check Nat.half Nat.zero
-- Nat.half Nat.zero : Nat

13.4.2. 管道语法 Pipeline Syntax

管道表示 Pipeline Notation 提供了另一种书写函数项应用的方式。 连续使用的管道符号可以借助解析优先级来 实现嵌套的函数项应用(位置实参),从而不必使用嵌套的圆括号。

语法:函数项的应用(左、右管道表示)

左管道表示会把管道左侧的项应用于右侧的项。

term ::= ...
  | term <| term

右管道表示会把管道右侧的项应用于左侧的项。

term ::= ...
  | term |> term

右管道表示直观地演示了数据的流向: 左侧的数据被传递给第一个函数项,然后 应用结果再被传递给第二个函数项,依此类推;而 左管道表示则与前者相反。

13-04-例子-04:函数项应用的右管道表示

右管道表示可以将一个项连续传入一系列函数项。对读者而言, 右管道表示偏向于强调数据的变换流程。

#eval "Hello!" |> String.toList |> List.reverse |> List.head!
-- '!'

13-04-例子-05:函数项应用的左管道表示

左管道表示可以将一个项连续传入一系列函数项。对读者而言, 左管道表示偏向于强调函数项的复合。

#eval List.head! <| List.reverse <| String.toList <| "Hello!"
-- '!'

语法:函数项的应用(右管道字段表示)

还有一种用于广义字段表示法的管道表示。

term ::= ...
  | term |>.ident

term ::= ...
    | term |>.fieldIdx

e |>.f arg 是 (e).f arg 的语法糖,但是后者可能要使用圆括号来避免歧义。

13-04-例子-06:函数项应用的右管道字段表示

由于形参顺序的原因,某些函数项的应用不适合用管道表示。 比如 Array.push 的首个形参是一个数组而非自然数项, 这就会导致如下错误:

#eval #[1, 2, 3] |> Array.push 4
-- failed to synthesize instance of type class
--   OfNat (Array ?m.4) 4
-- numerals are polymorphic in Lean, but the numeral `4` cannot be used in a context where the expected type is
--   Array ?m.4
-- due to the absence of the instance above
-- 
-- Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.

使用右管道字段表示则会将数组项插入到第一个类型匹配的位置

#eval #[1, 2, 3] |>.push 4
-- #[1, 2, 3, 4]

这一过程可以反复进行:

#eval #[1, 2, 3] |>.push 4 |>.reverse |>.push 0 |>.reverse
-- #[0, 1, 2, 3, 4]

13.5. 数值字面量 Numeric Literals

数值字面量有两种:一种是自然数字面量,另一种是科学计数字面量。 两者都是通过类进行重载的。

13.5.1. 自然数 Natural Numbers

13.5.2. 科学计数 Scientific Notation

13.5.3. 字符串 Strings

13.5.4. 列表和数组 Lists and Arrays

13.8. 模式匹配 Pattern Matching

模式匹配 Pattern Matching 是一种识别和解构项的方式。 它使用的模式 Pattern 语法是项语法的一个子集。 用于识别和解构值的模式的语法与用于构造该项的语法类似。 一或多个待匹配判别式 Match Discriminant 会同时与 一系列匹配分支 Match Alternative 进行比较。 待匹配判别式可被命名。 每个匹配分支都包含一或多个以逗号分隔的模式序列; 所有模式序列包含的模式个数必须与待匹配判别式的个数相同。 一个模式序列在成功匹配所有待匹配的判别式后, 紧跟在与之对应的 => 后面的项将会在一个被扩展的环境中求值: 该环境含有 各个模式变量的值,外加 各个被命名的待匹配判别式与模式相等的假设。 这个被求值项被称为匹配分支的右侧 Right-Hand Side。

语法:模式匹配
term ::= ...
  | match
        ((generalizing := (trueVal | falseVal)))?
        ((motive := term))?
        matchDiscr,*        -- 待匹配判别式序列
      with
    (| (term,*)|* => term)* -- 匹配分支,当中 term,* 是模式序列,term 是右侧

语法:待匹配判别式
matchDiscr ::=
  | term
  | ident : term

模式匹配项也可以将准引用用作模式: 它用于匹配对应的 Lean.Syntax 值,并将反引用的内容视为普通模式。 由于引用模式的编译方式与其他模式不同,因此 如果 match 当中存在一个模式是语法, 那么所有模式都必须是语法。引用模式见引用一节。

模式是项的一个子集。它们包含如下内容:

  • 匹配一切模式 Catch-All Patterns

    空洞语法 _ 可以被用作一种模式,它 将会匹配任何值,并且 不会绑定任何模式变量。 匹配一切模式并不完全等同于不使用模式变量: 它们可以被用在一些特殊的位置,比如 模式的类型需要更具体的不可访问模式;而 模式变量无法在这些位置被使用。

  • 标识符 Identifiers

    如果一个标识符 没有在当前作用域中被绑定,并且也 没有被应用于实参, 那么它便代表一个模式变量。 模式变量 Pattern Variable 可以匹配任何值, 被匹配的值会在局部环境中被绑定到模式变量上; 在该局部环境中,匹配分支的右侧会被求值。 如果标识符已经被绑定,那么只要它满足下述两个条件中的其中一个,它就可以被用作模式: 一是该标识符被绑定到一个归纳项构造子, 二是该标识符的声明带有 match_pattern 属性。

  • 函数项的应用 Applications

    函数项的应用也可以被用作模式, 只要被应用的函数是标识符,并且 该标识符被绑定到一个归纳项构造子,或者是 该标识符具有 match_pattern 属性且所有实参也都构成模式。 如果标识符是一个归纳项构造子, 那么该模式会匹配由该项构造子构造的值—— 前提是所有作为实参的模式都能匹配项构造子对应的实参。 如果标识符是一个具有 match_pattern 属性的函数, 那么函数项的应用会被展开, 并且展开后的项的规范形式会被用作模式。 默认实参会被按常规方式插入,并且 它们的规范形式会被用作模式;然而 省略号会导致所有后续实参都被视为通用模式, 即便这些实参可能带有相关的默认值或者策略。

  • 字面量 Literals

    字符字面量、字符串字面量也可以被用作模式, 它们会匹配对应的字符或字符串。 原始字符串字面量也可以被用作模式,但是 插值字符串则不行。模式中的 自然数字面量的解释则是通过 合成对应的 OfNat 类实例并 将结果项化简为规范形式(规范形式必须是模式)来完成的。同样地, 科学计数字面量则会通过对应的 OfScientific 类实例来解释。

  • 结构项的实例表示法 Structure Instances

    结构项的实例表示法也可以被用作模式。 它们会被解释为对应的结构项构造子。

  • 被引用的名称 Quoted Names

    诸如 `x 和 ``none 这样的被引用名称 会匹配对应的 Lean.Name 值。

  • 宏 Macros

    模式中的宏会被展开。 如果展开后的结果是模式, 那么宏就可以被用作模式。

  • 不可访问模式 Inaccessible patterns

    不可访问模式是那些由于后续类型约束而被迫具有特定值的模式。 任何项都可以被用作不可访问项。 不可访问项写在圆括号中,并且前面会带有一个点(.)。

语法:不可访问模式
term ::= ...
  | .(term)

13-06-例子-01:不可访问模式

下面是自然数的奇偶性的定义:

inductive Parity : Nat → Type where
  | even (h : Nat) : Parity (h + h)
  | odd (h : Nat) : Parity ((h + h) + 1)

def Nat.parity (n : Nat) : Parity n :=
  match n with
  | 0 => .even 0
  | n' + 1 => match n'.parity with
    | .even h => .odd h
    | .odd h =>
      have eq : (h + 1) + (h + 1) = (h + h + 1 + 1) :=
        by omega
      eq ▸ .even (h + 1)

由于一个自然数对应的 Parity 归纳项 以该数的一半(向下取整)作为其偶奇性表示的一部分, 因此除以二的操作就可以(以一种非常规的方式)如下实现: 首先获得该数对应的 Parity 归纳项, 然后再从归纳项中提取出这个数的一半。

def half (n : Nat) : Nat :=
  match n, n.parity with
  | .(h + h),     .even h => h
  | .(h + h + 1), .odd h  => h

由于归纳项构造子 Parity.even 和 Parity.odd 里的索引 强制要求自然数 n 必须具有某种形式(比如 h + h 或者 h + h + 1), 而这种形式在其他情况下并不是一个有效的模式, 因此匹配它们的模式必须使用不可访问模式以匹配被除的数。

模式也可以被命名。被命名的模式 会将自己的名称与一个模式关联起来; 在后续的模式以及匹配分支的右侧中, 该名称指代的是值当中被对应的模式所匹配的那一部分。 要为模式命名,需要在名称与模式之间使用 @ 分隔。 与待匹配判别式一样,具名模式也可以被提供名称以用于描述模式与判别式相等的假设。

语法:具名模式
term ::= ...
  | ident@term

term ::= ...
  | ident@ident:term

13.8.1. 类型 Types

每个待匹配判别式都必须是类型良好的。 由于模式是项的一个子集, 因此它们的类型也可以被检查。 每个用于匹配待匹配判别式的模式都必须与 对应的待匹配判别式具有相同的类型。

匹配分支右侧的类型必须与整个 match 项保持一致。 为了能支持依值类型,将待匹配判别式与模式进行匹配 会导致预期类型在模式的作用域内被细化。 在同一个匹配分支的后续模式以及右侧的类型中, 待匹配判别式的出现会被匹配它的模式所替换。

13-06-例子-02:模式匹配中的类型细化

下述类型索引族描述了一系列近乎平衡的树, 并且将深度编码在归纳类型当中:

inductive BalancedTree (α : Type u) : Nat → Type u where
  | empty : BalancedTree α 0
  | branch
    (left : BalancedTree α n)
    (val : α)
    (right : BalancedTree α n) :
    BalancedTree α (n + 1)
  | lbranch
    (left : BalancedTree α (n + 1))
    (val : α)
    (right : BalancedTree α n) :
    BalancedTree α (n + 2)
  | rbranch
    (left : BalancedTree α n)
    (val : α)
    (right : BalancedTree α (n + 1)) :
    BalancedTree α (n + 2)

为了实现一个能够借助 一些初始元素和 一个给定深度 构造出一个完美平衡树的函数, 我们可以先使用一个空洞来定义该函数。 下述错误信息表明该树应该具有指定的深度。

def BalancedTree.filledWith
  (x : α) (depth : Nat) : BalancedTree α depth 
  := _
-- 第一个空洞导致如下报错:
-- don't know how to synthesize placeholder
-- context:
--  α:Type u
--  x:α
--  depth:Nat
-- ⊢ BalancedTree α depth

对预期深度进行匹配并插入空洞 会导致每个空洞都出现一个错误信息。 这些信息表明预期类型已经被细化, depth 已被替换为模式对应的值。

def BalancedTree.filledWith
  (x : α) (depth : Nat) :
  BalancedTree α depth 
  :=match depth with
    | 0 => _
    | n + 1 => _
-- 第一个空洞导致如下报错:
-- don't know how to synthesize placeholder
-- context:
--   α:Type u
--   x:α
--   depth:Nat
-- ⊢ BalancedTree α 0 -- depth 已被替换为 0
-- 第二个空洞导致如下报错:
-- don't know how to synthesize placeholder
-- context:
--   α:Type u
--   x:α
--   depth n:Nat
-- ⊢ BalancedTree α (n + 1) -- depth 已被替换为 n + 1

同时对树的深度和树本身进行匹配 会导致树的类型会按照 depth 的模式被细化。 这意味着某些组合并非类型良好的(比如下面的 0 和 branch), 因为细化第二个待匹配判别式的类型会得到 BalancedTree α 0, 而这与归纳项构造子的类型不匹配。

def BalancedTree.isPerfectlyBalanced
  (n : Nat) (t : BalancedTree α n) : Bool 
  :=match n, t with
    | 0, .empty => true
    | 0, .branch left val right => -- 此处报错
      isPerfectlyBalanced left &&
      isPerfectlyBalanced right
    | _, _ => false
-- Type mismatch
--   left.branch val right
-- has type
--   BalancedTree ?m.13 (?m.12 + 1)
-- but is expected to have type
--   BalancedTree α 0

13.8.1.1. 模式与待匹配判别式相等的证明 Pattern Equality Proofs

在一个待匹配判别式被命名后,match 会 生成一个证明以证明对应的模式与待匹配判别式是相等的,并将该证明 绑定到在匹配分支右侧中被提供的名称上。 对于 类型索引族上的依值模式匹配,以及 期望命题作为显式实参的 API, 这么做 有助于填补两者之间的空缺,并且还能 帮助那些可能会用到该假设的策略完成证明。

13-06-例子-03:模式与待匹配判别式相等的证明

函数常量 last? 要么 抛出异常,要么借助标准库函数 List.getLast 返回列表实参的最后一个元素。 该函数常量期望一个该列表实参是非空的证明。 为针对 xs 的匹配命名可以确保 作用域中存在一个假设能表明 xs 等于 _ :: _, 然后 simp_all 便可以使用该假设来证明目标。

def last? (xs : List α) : Except String α :=
  match h : xs with
  | [] =>
    .error "Can't take first element of empty list"
  | _ :: _ =>
    .ok <| xs.getLast (show xs ≠ [] by intro h'; simp_all)

如果没有为匹配命名, 那么 simp_all 就无法找到矛盾。

def last?' (xs : List α) : Except String α :=
  match xs with
  | [] =>
    .error "Can't take first element of empty list"
  | _ :: _ =>
    .ok <| xs.getLast (show xs ≠ [] by intro h'; simp_all)
-- simp_all made no progress

13.8.1.2. 显式的动机 Explicit Motives

模式匹配并不是一个 Lean 内置的原语。相反, 它会被翻译为归纳项递归子的应用,这是通过辅助匹配函数实现的。 归纳项递归子以及辅助匹配函数都需要一个动机来解释 待匹配判别式与 结果类型之间的关系。 一般来说,match 对应的阐述器 能够合成一个适当的动机,并且 模式匹配的过程中出现的类型细化 是由所选择的动机所导致的。 在某些特殊情况下可能需要用到一个不同的动机,并且 该动机可能会通过 (motive := …) 语法显式地提供。 该动机应该是一个函数类型,它需要接收的实参个数至少得 与待匹配判别式的个数相同。 将该函数类型应用于按顺序排列的待匹配判别式所得到的类型 就是整个 match 项的类型,而 将该函数类型应用于每个匹配分支中所有模式所得到的类型 就是该匹配分支右侧的类型。

13-06-例子-04:模式匹配中使用显式动机

显式提供的动机可以用于提供类型信息, 这些信息在周围的语境中可能是无法获得的。 尝试同时匹配一个数字以及其是 5 的证明会导致错误, 因为没有充分的理由来将该数字与该证明联系起来:

#eval
  match 5, rfl with
  | 5, rfl => "ok"
-- Invalid match expression: This pattern contains metavariables:
--   Eq.refl ?m.14

一个显式的动机可以被用来解释 该数字与该证明之间的关系:

#eval
  match (motive := (n : Nat) → n = 5 → String) 5, rfl with
  | 5, rfl => "ok"
-- "ok"

13.8.1.3. 待匹配判别式的细化 Discriminant Refinement

在对类型索引族进行匹配的时候,索引也必须是判别式; 否则该模式就不是类型良好的。如果索引只是一个变量, 但是归纳项构造子所要求的类型需要一个更具体的值,那么就会出现类型错误。然而, 一种称为判别式细化的过程会自动将索引作为额外的判别式添加进来。

13-06-例子-05:模式匹配带索引的归纳项时会对判别式自动细化

在下述 f 的声明中,等式的证明是唯一的判别式;然而 等式 Eq 是一个索引族,并且只有当额外的判别式 n 被添加进来时 匹配才能是有效的。

def f (n : Nat) (p : n = 3) : String :=
  match p with
  | rfl => "ok"

使用 #print 可以查看 被自动添加的额外的判别式。

#print f
-- def f : (n : Nat) → n = 3 → String :=
-- fun n p =>
--   match 3, p with
--   | .(n), ⋯ => "ok"

13.8.1.4. 泛化 Generalization

模式匹配阐述器会自动确定动机,这是通过 在预期类型中查找判别式的出现完成的;阐述器 会在后续判别式的类型中对这些出现的位置进行泛化, 如此合适的模式便可将其替换。此外 对于语境中变量的类型,判别式在其中的出现 也会默认被泛化并被替换掉。这种行为 可以通过向 match 传递 (generalizing := false) 来禁用。

13-06-例子-06:模式匹配中泛化启用与禁用的区别

在下面有关常量 boolCases 的声明中,b 会被泛化到 h 的类型中,然后 再被替换为实际的模式;这意味着 ifTrue 和 ifFalse 在各自的情况下分别具有类型 true = true → α 和 false = false → α,但 h 的类型提到了原始的待匹配判别式。

def boolCases (b : Bool)
    (ifTrue : b = true → α)
    (ifFalse : b = false → α) :
    α :=
  match h : b with
  | true  => ifTrue  h -- 换成 rfl 就可以运行
  | false => ifFalse h -- 换成 rfl 就可以运行
-- Application type mismatch: The argument
--   h
-- has type
--   b = true
-- but is expected to have type
--   true = true
-- in the application
--   ifTrue h

关闭泛化后类型检查就可以成功,因为 b 被保留在 ifTrue 和 ifFalse 的类型中。

def boolCases (b : Bool)
    (ifTrue : b = true → α)
    (ifFalse : b = false → α) :
    α :=
  match (generalizing := false) h : b with
  | true  => ifTrue  h
  | false => ifFalse h

在被泛化的版本中也可以换成用 rfl 作为证明实参。

13.8.2. 自定义模式函数 Custom Pattern Functions

在模式中,带 match_pattern 属性的已声明常量不会被拒绝, 而是会被展开并被规范化;这使得我们可以在许多模式中使用更便捷的语法。标准库当中的 Nat.add、HAdd.hAdd、Add.add、Neg.neg 都具有该属性, 如此便可以使用形如 n + 1 的模式,而非 Nat.succ n;类似地, Unit、Unit.unit 对应的常量声明会将 PUnit 和 PUnit.unit 的相应宇宙层级形参设置为 0; Unit.unit 上的 match_pattern 属性 使得它可以被用于模式中,在其中它会被展开为 PUnit.unit.{0}。

属性:match_pattern

match_pattern 属性用于指明 一个常量在模式中应该被展开,而不是被拒绝。

attr ::= ...
  | match_pattern

13-06-例子-07:匹配模式遵循归约

下述函数无法被编译:

def nonzero (n : Nat) : Bool :=
  match n with
  | 0 => false
  | 1 + k => true

模式 1 + _ 处的报错信息如下:

-- Invalid pattern(s): 
-- `k` is an explicit pattern variable, 
-- but it only occurs in positions that are 
-- inaccessible to pattern matching:
--   .(Nat.add 1 k)

这是由于 Nat.add 的声明是通过 对其第二个形参进行递归实现的,等价于:

def add : Nat → Nat → Nat
  | a, Nat.zero   => a
  | a, Nat.succ b => Nat.succ (Nat.add a b)

这里无法进行 ι 归约, 因为待匹配的值是一个变量而非一个归纳项构造子。 1 + k 会卡在 Nat.add 1 k,而后者不是一个有效的模式。

如果是换成 k + 1(也就是 Nat.add k (.succ .zero)), 那么第二个模式就能匹配上判别式,判别式会被归约为 Nat.succ (Nat.add k .zero); 接着第二个模式再次匹配成功,判别式被进一步归约为 Nat.succ k,而这是一个有效的模式。

13.8.3. 函数项的构造(模式匹配)Pattern Matching Functions

语法:函数项的构造(模式匹配)

函数项的构造也可以使用模式匹配的方式来实现,这 是通过在 fun 后面补充竖线 (|) 来实现的。

term ::= ...
  | fun
    (| term,* => term)*

上述语法会被脱糖为一个函数项, 其会立即对其实参进行模式匹配。

13-06-例子-08:函数项的构造使用模式匹配语法

isZero 是使用模式匹配函数抽象表达式所声明的,而 isZero' 是使用模式匹配表达式所声明的:

def isZero : Nat → Bool :=
  fun
    | 0 => true
    | _ => false

def isZero' : Nat → Bool :=
  fun n =>
    match n with
    | 0 => true
    | _ => false

由于前者是后者的语法糖, 因此它们是定义等价的:

example : isZero = isZero' := rfl

通过 #print 可以观察到脱糖的结果:

#print isZero
-- def isZero : Nat → Bool :=
-- fun x =>
--   match x with
--   | 0 => true
--   | x => false

#print isZero'
-- def isZero' : Nat → Bool :=
-- fun n =>
--   match n with
--   | 0 => true
--   | x => false

13.8.4. 其他模式匹配运算符 Other Pattern Matching Operators

除了 match 和 if let, 还有一些额外的运算符也可以执行模式匹配。

语法:matches 运算符

matches 在左侧的项匹配右侧的模式时 会返回 true,否则便会返回 false。

term ::= ...
  | term matches term

在对 matches 的结果进行分支时, 通常最好使用 if let:它 不仅可以检查模式是否匹配, 还可以绑定模式变量。

When branching on the result of matches, it’s usually better to use if let, which can bind pattern variables in addition to checking whether a pattern matches.

如果不存在能够匹配判别式的归纳项构造子模式, 那么相关代码就是不可达的;因为 在局部语境中必然存在一个错误的假设。 nomatch 表达式是一个不存在任何分支的模式匹配, 只要不存在能够匹配判别式的分支, 那么 nomatch 就可以具有任何类型。

语法:模式匹配无分支
term ::= ...
  | nomatch term,*

13-06-例子-09:不一致的索引

对于下面的例子,能够同时匹配两个判别式的归纳项构造子模式是不存在的:

example (p1 : x = "Hello") (p2 : x = "world") : False :=
  nomatch p1, p2

这是因为它们分别将 x 的值细化为不相等的字符串。因此, nomatch 运算符允许 example 声明主体证明 False(或者是任何其他命题或类型)。

如果预期类型是一个函数类型,那么 那么便可用 nofun:nofun 是一个函数项的简写形式, 它接收的形参个数与函数类型所指示的形参个数相同,并且 其函数项构造的主体是将 nomatch 应用于所有形参后的结果。

语法:函数项的构造(无匹配分支)
term ::= ...
  | nofun

13-06-例子-10:不可能的函数类型

与其为两个等式的证明引入实参并 在 nomatch 中使用它们, 不如直接使用 nofun。

example : x = "Hello" → x = "world" → False := nofun

13.10. 类型注解 Type Ascriptions

类型注解显式地为项添加类型信息, 它们是为 Lean 指明项的预期类型的一种方式。 被注解的这个类型必须定义等同于 Lean 基于项所处语境所推断出的类型。 类型注解不仅仅用于记录程序:

  • 程序文本可能没有足够的信息用于推断项的类型。 类型注解是提供类型信息的一种方式。

  • 项被推断出的类型可能与用户预期类型不符 程序文本可能包含足够的信息用于推导项的类型, 但推导出的类型可能不是用户所期待的。

  • 项的预期类型会驱动 Lean 插入类型强制转换, 而类型注解是控制强制转换插入位置的一种方式。

语法:类型后缀注解

类型注解必须置于圆括号当中。 它们指示第一个项的类型是第二个项。

term ::= ...
  | ([anonymous]term : term)

某些地方需要的类型注解可能会很冗长,比如策略证明或者是 do 代码块。 在上述情况中类型后缀注解可能会难以阅读,因为必须用到圆括号;此外 项的类型对于理解证明和 do 代码块是至关重要的。 在这些情况下,前缀版本的类型注解可能更易于阅读。

语法:类型前缀注解
term ::= ...
  | show term from term

当 show 表达式内的 term 是策略证明时, 关键字 from 可被省略。

term ::= ...
  | show term by tacticSeq

13-07-例子-01:类型注解用在证明里

上述例子无法正常执行策略风格证明, 因为 Lean 无法推断出用户期待证明的命题。 在运行前方的策略时, 命题会被自动细化 为一个可被策略证明的命题。 然而默认情况下它会被细化为一个错误的命题, 从而导致证明失败。

example (n : Nat) := by
  induction n
  next => rfl
  next n' ih =>
    simp only [HAdd.hAdd, Add.add, Nat.add] at *
    rewrite [ih]
    rfl
-- Invalid rewrite argument: Expected an
--   equality or
--   iff proof or
--   definition name, 
-- but `ih` is a proof of
--   0 ≍ n'

期待被证明的命题可通过 show 来进行类型前缀注解。 在某些语法语境中,如果添加局部声明的代价很大, 那么类型前缀注解就可以排上用场:

example (n : Nat) := show 0 + n = n by
  induction n
  next => rfl
  next n' ih =>
    simp only [HAdd.hAdd, Add.add, Nat.add] at *
    rewrite [ih]
    rfl

13-07-例子-02:类型注解用在 do 代码块里

下述例子缺乏足够的类型信息来推断 Pure 类实例。

example := do
  return 5
-- typeclass instance problem is stuck
--   Pure ?m.12
-- 
-- Note: Lean will not try to resolve this typeclass instance problem 
-- because the type argument to `Pure` is a metavariable. 
-- This argument must be fully determined 
-- before Lean will try to resolve the typeclass.
-- 
-- Hint: Adding type annotations and supplying implicit arguments to functions 
-- can give Lean more information for typeclass resolution. For example, 
-- if you have a variable `x` that you intend to be a `Nat`, 
-- but Lean reports it as having an unresolved type like `?m`, 
-- replacing `x` with `(x : Nat)` can get typeclass resolution un-stuck.

使用 show 的类型前缀注解与 hole 连用 可以用来指明单子。 默认的 OfNat _ 5 类实例提供的类型信息 足以将 hole 自动填充为 Nat。

example := show StateM String _ from do
  return 5

类型前缀注解和 show 之间有个很重要的差异: 普通的类型后缀注解改变了项的预期类型, 这可能会改变项的阐述方式;在被阐述后 Lean 会推断出结果项的类型,并 将被推断出的类型用于后续的阐述任务。 另一方面,show 会阐述为一个项, 其推断出的类型就是被注解的类型。 广义字段表示方式 可以用来观察到这样的差异,其中被注解的类型 只有在使用 show 时才会被用于解析字段。

13-07-例子-03:类型前缀、后缀注解影响代码结果

下述定义为 List String 构建了一个别名:

def Colors := List String

尽管类型后缀注解提供了必要的类型信息 来确定 List.nil 的隐式参数 String, 但是最终推导出的类型依然是 List String:

#check ([] : Colors)
-- [] : List String

相反如果用的是 show, 那么待阐述的项在构建时类型就会被推断为 Colors:

#check (show Colors from [])
-- have this := [];
-- this : Colors

下述函数被设计成通过广义字段表示法来调用:

def Colors.hasYellow (cs : Colors) : Bool :=
  cs.any (·.toLower == "yellow")

由于类型推断结果不同,它 不能与类型后缀注解一起使用,但 可以与 show 一起使用。

#eval ([] : Colors).hasYellow
-- Invalid field `hasYellow`: The environment 
-- does not contain `List.hasYellow`, so 
-- it is not possible to project the field `hasYellow` 
-- from an expression
--   []
-- of type `List String`

#eval (show Colors from []).hasYellow
-- false

索引 Index

术语

语法

常量

定理

公理

归纳

结构

类

选项

  • pp.fieldNotation: 默认为 true(见 §13-4-1)
    是否在美观打印时以字段表示法书写所有函数项的应用 —— 前提是属性 @[pp_nodot] 未被启用

属性

例子

  • 13-01-例子-01: 名称解析时内层局部声明优先
    示范:x
  • 13-01-例子-02: 名称解析时命名空间前缀更长的优先
    示范:A.x,A.B.C.x
  • 13-01-例子-03: 名称解析时标识符前缀更长的优先
    示范:y.y.y,A,B
  • 13-01-例子-04: 名称解析时当前命名空间优先级高于已开启的命名空间
    示范:A.x,B.x,C
  • 13-01-例子-05: 名称解析失败
    示范:A.x,B.x
  • 13-01-例子-06: 名称解析失败可以通过类型注解消除歧义
    示范:C.x,D.x
  • 13-01-例子-07: 名称调用的先导点号表示
    示范:(.replicate 3 () : List Unit)
  • 13-01-例子-08: 名称调用的先导点号表示以及名称的展开
    示范:(.replicate 3 () : MyList Unit)
  • 13-02-例子-01: 函数类型构造的多种表示方式
    示范:Nat → Nat → Nat
  • 13-03-例子-01: 函数项构造时形参以普通隐式、严格隐式方式声明的区别
    示范:id.{1}
  • 13-03-例子-02: 函数项构造时类型形参常以隐式方式声明
    示范:id.{1}
  • 13-04-例子-01: 函数项应用时位置实参和具名实参的混合使用
    示范:sum3
  • 13-04-例子-02: 函数项应用的字段表示以及类型常量的展开
    示范:Username,String
  • 13-04-例子-03: 函数项应用的打印在字段表示启用、禁用时的区别
    示范:Nat.half
  • 13-04-例子-04: 函数项应用的右管道表示
    示范:"Hello!".toList.reverse.head!
  • 13-04-例子-05: 函数项应用的左管道表示
    示范:"Hello!".toList.reverse.head!
  • 13-04-例子-06: 函数项应用的右管道字段表示
    示范:((#[1, 2, 3].push 4).reverse.push 0).reverse
  • 13-06-例子-01: 不可访问模式
    示范:Parity,Nat.parity,Nat.half
  • 13-06-例子-02: 模式匹配中的类型细化
    示范:BalancedTree,.filledWith,.isPerfectlyBalanced
  • 13-06-例子-03: 模式与待匹配判别式相等的证明
    示范:last?,last?',List.getLast
  • 13-06-例子-04: 模式匹配中使用显式动机
    示范:match 5, rfl with | 5, rfl => "ok"
  • 13-06-例子-05: 模式匹配带索引的归纳项时会对判别式自动细化
    示范:f (n : Nat) (p : n = 3) : String,Eq
  • 13-06-例子-06: 模式匹配中泛化启用与禁用的区别
    示范:boolCases
  • 13-06-例子-07: 匹配模式遵循归约
  • 13-06-例子-08: 函数项的构造使用模式匹配语法
  • 13-06-例子-09: 不一致的索引
    示范:x = "Hello" → x = "world" → False
  • 13-06-例子-10: 不可能的函数类型
    示范:x = "Hello" → x = "world" → False
  • 13-07-例子-01: 类型注解用在证明里
    示范:TODO
  • 13-07-例子-02: 类型注解用在 do 代码块里
    示范:TODO
  • 13-07-例子-03: 类型前缀、后缀注解影响代码结果
    示范:TODO
给我买杯饮料!
采石场的主人 微信微信