Lean 语言参考手册

7.6. 递归定义🔗

允许任意递归函数定义会使 Lean 的逻辑不一致。一般递归使得可以写出环形证明:“命题 P 为真,因为命题 P 为真”。在证明之外,一个无限循环可以被赋予类型 Empty,再结合 Lean.Parser.Term.nomatch : termEmpty match/ex falso. `nomatch e` is of arbitrary type `α : Sort u` if Lean can show that an empty set of patterns is exhaustive given `e`'s type, e.g. because it has no constructors. nomatchEmpty.rec,即可“证明”任意定理。

直接禁止递归函数定义将大幅降低 Lean 的实用性:归纳类型是定义谓词与数据的关键,而它们本身具有递归结构。 此外,多数有用的递归函数并不威胁自洽性,而无限循环通常意味着定义有误而非有意为之。 Lean 并未一禁了之,而是要求每个递归函数都以安全的方式定义。 在精译递归定义的过程中,Lean 的精译器还会同时给出该定义安全的理由。可参阅精译概览中的 精译器的输出一节,了解递归定义精译在整体精译流程中的位置。

可以定义的递归函数主要有六类:

结构递归函数

结构递归函数接收某个实参,并且仅在该实参的真子项上进行递归调用。严格来说,类型为 索引族 的实参会与其索引成组,把整个集合视作一个整体。 精译器会把递归翻译成对该实参的 递归器 的调用。 由于每个类型正确的递归器使用都保证避免无限回归,这样的翻译即构成函数终止性的证据。 通过递归器定义的函数应用在定义上等同于递归结果,并且在内核中通常较为高效。

良基关系上的递归

有些函数也难以改写为结构递归;例如,某个函数之所以终止,是因为随着数组索引增大,索引与数组长度之差在减小,但此时由于增长的是函数的实参本身,Nat.rec 并不适用。 在这种情形下,存在一个随每次递归调用而减少的终止度量,但该度量本身并非函数的一个实参。 这时可以使用 良基递归 来定义函数。 良基递归是一种技术:系统地把“伴随度量递减的递归函数”转化为“基于证明的递归函数”,该证明表明任意度量递减序列最终会在最小值处终止。 用良基递归定义的函数应用不一定与其返回值在定义上相等,但这种相等可以作为命题来证明。 即便存在定义相等,这类函数在计算上仍常常较慢,因为它们需要归约通常很大的证明项。

作为偏不动点的递归函数

一个函数的定义可以理解为一条给出其行为的方程。 在某些情况下,即使该递归函数对所有输入未必终止,仍可证明存在一个满足此规格的函数。 该策略甚至适用于某些函数定义对所有输入未必终止的情形。 由此得到的偏函数作为这些方程的不动点而出现,被称为 偏不动点

尤其是,返回类型位于某些单子中的函数(例如 Option)可以用该策略来定义。 对这类单子函数,Lean 还会生成额外的偏正确性定理。 与良基递归类似,按偏不动点定义的函数应用在定义上不等同于其返回值,但 Lean 会生成定理,在命题层面将该函数与其展开式以及定义中所给的归约行为相等同。

作为不动点的余归纳与归纳谓词

取值于 Prop 的递归函数,可以定义为完备格上单调算子的最大不动点或最小不动点。 余归纳谓词使用 Lean.Parser.Command.declaration : commandcoinductive_fixpointLean.Parser.Command.declaration : commandcoinductive 命令定义,用来描述无限序列、互模拟等潜在的无限行为。 归纳谓词使用 Lean.Parser.Command.declaration : commandinductive_fixpoint 定义;它提供了标准归纳类型之外的另一种选择,并可用于归纳—余归纳混合互递归块。

余域非空的偏函数

在许多应用中,某些函数的具体实现并不需要被推理。 一个递归函数可能仅作为证明自动化步骤实现的一部分,或仅是不会被形式化证明正确性的普通程序。 在这类场景中,Lean 内核不需要该定义在“定义相等”或“命题相等”层面成立;只要保持逻辑自洽即可。 被标记为 Lean.Parser.Command.declaration : commandpartial 的函数会被内核视作不透明常量,既不会被展开也不会被归约。 为保持自洽性,唯一的要求是其返回类型可被占据。 偏函数在编译后的代码中仍可照常使用,也可出现在命题与证明中;只是它们在 Lean 逻辑中的等式理论非常薄弱。

不安全的递归定义

不安全定义不受偏定义的任何限制。 它们可自由使用一般递归,并可使用会打破等式理论假设的 Lean 特性,例如强制转换原语(unsafeCast)、检查指针相等(ptrAddrUnsafe),以及观察引用计数isExclusiveUnsafe)。 但凡引用不安全定义的声明本身也必须标记为 Lean.Parser.Command.declaration : commandunsafe,以清楚表明此处不保证逻辑自洽。 在编译后的代码中,不安全操作可用于以更高效的实现替换其他函数的实现,而内核仍然使用原始定义。 被替换的函数可以是不透明的,此时该函数名在逻辑中的等式理论是平凡的;也可以是普通函数,此时逻辑中仍会使用该函数。 请谨慎使用这一特性:逻辑自洽性虽不受威胁,但若不安全实现有误,Lean 程序的实际行为可能会偏离其经验证的逻辑模型。

精译器输出概览所述,递归函数的精译分为两个阶段:

  1. 先假定 Lean 的内核类型论允许递归定义,对定义进行精译。 除递归调用外,这个临时定义已被完整精译;编译器也从这些临时定义生成代码。

  2. 随后进行终止性分析,尝试使用五种技术向 Lean 内核说明该函数是安全的。 若定义标有 Lean.Parser.Command.declaration : commandunsafeLean.Parser.Command.declaration : commandpartial,则采用相应技术。 若存在显式的 Lean.Parser.Command.declaration : commandtermination_byLean.Parser.Command.declaration : commandpartial_fixpointLean.Parser.Command.declaration : commandcoinductive_fixpointLean.Parser.Command.declaration : commandinductive_fixpoint 子句,则只尝试该子句指定的技术。 若不存在这些子句,精译器会进行搜索:依次把函数的每个形参作为结构递归候选,并尝试寻找一个在每次递归调用时沿良基关系递减的度量。

本节描述支配递归函数的规则。介绍互递归之后,将逐一说明五种递归定义技术,并讨论各自推理能力与灵活性之间的权衡。

7.6.1. 互递归🔗

就像递归定义是在其定义体中提到正在被定义的名字一样,互递归 的定义指的是:它们本身可以是递归的,或彼此相互引用。 要在多个声明之间使用互递归,必须把它们放入一个 互递归块 中。

语法互递声明块

互递的一般语法为:

command ::= ...
    | mutual
        declaration*
      end

其中各声明必须是定义或定理。

在一个互递声明块中,各声明的名称不在彼此的类型签名的作用域内,但在彼此的定义体中可见。 尽管这些名称不在签名的作用域内,它们也不会被当作自动绑定的隐式参数插入。

互递声明块的作用域

在互递声明块中定义的名称不在彼此的签名作用域内。

mutual abbrev NaturalNum : Type := Nat def n : Unknown identifier `NaturalNum`NaturalNum := 5 end
Unknown identifier `NaturalNum`

若不使用互递块,该定义即可通过:

abbrev NaturalNum : Type := Nat def n : NaturalNum := 5
互递块的作用域与自动隐式参数

在互递声明块中定义的名称不在彼此的签名作用域内。不过,它们也不能作为自动绑定的隐式参数使用:

mutual abbrev α : Type := Nat def identity (x : Unknown identifier `α`α) : Unknown identifier `α`α := x end
Unknown identifier `α`

若改用不同的名称,则会自动添加该隐式参数:

mutual abbrev α : Type := Nat def identity (x : β) : β := x end

递归定义的精译总是在互递块这一粒度上进行;即便某个声明并不处在互递块中,也会好比其周围包了一层单元素的互递块。 通过 Lean.Parser.Term.letrec : termlet recLean.Parser.Command.declaration : commandwhere 引入的局部定义会被从其上下文提升出去;必要时为捕获到的自由变量引入参数;并被视作 Lean.Parser.Command.mutual : commandmutual 块中的独立定义。 因此,写在 Lean.Parser.Command.declaration : commandwhere 块中的辅助定义,既可以彼此互递归,也可以和所在的主体定义互递归,但它们不能在彼此的类型签名中相互引用。

在精译的第一步结束后(此时定义仍是递归的),在使用上述技术消解递归之前,Lean 会在互递块中的这些定义里识别出真正(互相)递归的团簇,并按照依赖顺序分别处理它们。

7.6.2. 结构递归🔗

结构递归函数是指每次递归调用都作用于相对于该实参在结构上更小的项的函数。 所有递归调用中都必须是同一个形参变小;这个形参称为 递减参数。 结构递归比递归器提供的原始递归更强,因为递归调用可以使用该实参更深层嵌套的子项,而不只是它的直接子项。 不过,实现结构递归所用的构造本身仍是基于递归器实现的;这些辅助构造见归纳类型一节

支配结构递归的规则在本质上是句法性的。 许多递归定义在计算行为上确实体现为结构递归,但并不会被这些规则接受;这是因为该分析必须完全自动化,这一限制是根本性的结果。 良基递归提供了一种证明终止性的语义方法,既可用于递归函数并非结构递归的情形,也可用于函数虽按结构递归计算、却不满足句法要求的情形。

结构递归与减法

函数 countdown 是结构递归的。 形参 n 与模式 n' + 1 进行匹配,这意味着在模式匹配的第二个分支中,n'n 的直接子项:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown n'

若把模式匹配替换为等价的布尔测试与减法,就会报错:

def fail to show termination for countdown' with errors failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' n' failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal n:Nath✝:¬(n == 0) = truen':Nat := n - 1n - 1 < ncountdown' (n : Nat) : List Nat := if n == 0 then [] else let n' := n - 1 n' :: countdown' n'
fail to show termination for
  countdown'
with errors
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' n'


failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
n:Nath✝:¬(n == 0) = truen':Nat := n - 1n - 1 < n

这是因为这里并没有对形参 n 做模式匹配。 虽然这个函数确实会终止,但其终止性的论证依赖于 if、相等测试和减法的性质,而不是 Nat 作为 归纳类型 的一般性特征。 这些论证要用 良基递归 来表达;只要对函数定义做一点改动,就能让 Lean 的良基递归自动支持构造出另一份终止性证明。 这个版本不是分支于 Nat 的布尔相等测试结果,而是分支于 命题相等 的可判定性:

def countdown' (n : Nat) : List Nat := if n = 0 then [] else let n' := n - 1 n' :: countdown' n'

这里,Lean 的自动化会依据命题相等和减法的事实自动构造终止性证明。 其底层采用的是良基递归,而不是结构递归。

结构递归既可以显式使用,也可以自动推断。 在显式结构递归中,函数定义会声明哪个形参是 递减参数。 若未显式声明终止性策略,Lean 会同时搜索递减参数,以及可供 良基递归 使用的递减度量。 显式标注结构递归有以下好处:

  • 可以加快精译,因为无需搜索。

  • 能为读者记录终止性论证。

  • 在明确希望使用结构递归的场景下,可以防止意外改用良基递归。

7.6.2.1. 显式结构递归🔗

若要显式使用结构递归,可以在函数或定理定义上添加 Lean.Parser.Command.declaration : commandtermination_by structural 子句,用以指定 递减参数。 递减参数可以引用签名中已命名的形参。 若签名写成函数类型,则递减参数还可以是签名中未命名的形参;此时可在箭头(Lean.Parser.Command.declaration : command=>)前写出其余形参的名称,将它们引入作用域。

指定递减参数

当递减参数是函数的具名形参时,可以直接引用其名称来指定。

def half (n : Nat) : Nat := match n with | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n

当递减参数在签名中未命名时,可以在 Lean.Parser.Command.declaration : commandtermination_by 子句中局部引入一个名称。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n => n
语法显式结构递归

termination_by structural 子句用来引入递减参数。

Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferred termination measure will be suggested.

terminationBy ::= ...
    | Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferred termination measure will be suggested.

termination_by structural (ident* =>)? term

可选 => 之前的标识符可以把尚未在声明头中绑定的函数形参带入作用域,而后面必需的项必须指明函数的某个形参,无论它是在声明头中引入,还是在该子句中局部引入。

递减参数必须满足下列条件:

  • 它的类型必须是 归纳类型

  • 若其类型是 索引族,则所有索引都必须是该函数的形参。

  • 若递减参数的归纳类型或索引族带有数据类型参数,则这些数据类型参数本身只能依赖属于 固定前缀 的函数形参。

固定参数 是指在所有递归调用中都原样传递、且不是递归参数类型之索引的函数形参。 固定前缀 是函数形参中满足“全部固定”的最长前缀。

不合格的递减参数

递减参数的类型必须是归纳类型。 在 notInductive 中,被指定为递减参数的是一个函数:

def notInductive (x : Nat Nat) : Nat := notInductive (fun n => x (n+1)) cannot use specified measure for structural recursion: its type is not an inductivetermination_by structural x
cannot use specified measure for structural recursion:
  its type is not an inductive

若递减参数是索引族,则所有索引都必须是变量。 在 constantIndex 中,索引族 Fin' 却被应用到了一个常量值上:

inductive Fin' : Nat Type where | zero : Fin' (n+1) | succ : Fin' n Fin' (n+1) def constantIndex (x : Fin' 100) : Nat := constantIndex .zero cannot use specified measure for structural recursion: its type Fin' is an inductive family and indices are not variables Fin' 100termination_by structural x
cannot use specified measure for structural recursion:
  its type Fin' is an inductive family and indices are not variables
    Fin' 100

递减参数类型中的参数,不能依赖那些位于变化参数或索引之后的函数形参。 在 afterVarying 中,固定前缀 为空,因为第一个形参 n 会变化,所以 p 不属于固定前缀:

inductive WithParam' (p : Nat) : Nat Type where | zero : WithParam' p (n+1) | succ : WithParam' p n WithParam' p (n+1) failed to infer structural recursion: Cannot use parameter x: failed to eliminate recursive application afterVarying (n + 1) p WithParam'.zero def afterVarying (n : Nat) (p : Nat) (x : WithParam' p n) : Nat := afterVarying (n+1) p .zero termination_by structural x
failed to infer structural recursion:
Cannot use parameter x:
  failed to eliminate recursive application
    afterVarying (n + 1) p WithParam'.zero

此外,函数的每次递归调用都必须作用于递减参数的某个 真子项

  • 递减参数自身是一个子项,但不是真子项。

  • 若某个子项是 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式或其他模式匹配语法的 判别项,则与该判别项匹配的模式,会成为各个 匹配分支右侧 中的子项。 尤其是,这里会使用 匹配泛化 的规则,把判别项与右侧中模式项的出现关联起来;因此它遵守 定义相等。 当且仅当判别项是真子项时,该模式才是真子项。

  • 若某个子项是作用于若干实参的构造器,那么它的递归实参都是真子项。

嵌套模式与子项

在下例中,递减参数 n 与嵌套模式 .succ (.succ n) 匹配。因此 .succ (.succ n)n 的一个(非严格)子项,于是 n.succ n 都是真子项,所以该定义会被接受。

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib n + fib (.succ n) termination_by structural n => n

为便于说明,这个例子使用 .succ n.succ (.succ n),而不是等价的、Nat 专用的 n+1n+2

对复杂表达式做匹配可能阻碍精译

在下例中,递减参数 n 并不是 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式的直接 判别项。 因此,n' 不会被视为 n 的子项。

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application half n' def half (n : Nat) : Nat := match Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    half n'

若改用 良基递归,并显式把判别项与匹配模式联系起来,这个定义就能被接受。

def half (n : Nat) : Nat := match h : Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by n decreasing_by n:Natn':Nath:n = n' + 1 + 1n' < n' + 1 + 1; All goals completed! 🐙

类似地,下面这个例子也会失败:虽然 xs.tail 会归约为 xs 的一个真子项,但按照上述规则,这一点对 Lean 来说并不可见。 特别地,xs.tailxs 的某个真子项并不 定义相等

failed to infer structural recursion: Cannot use parameter #2: failed to eliminate recursive application listLen xs.tail def listLen : List α Nat | [] => 0 | xs => listLen xs.tail + 1 termination_by structural xs => xs
结构递归中的同时匹配与匹配成对值

用于证明终止性的这些策略有一个重要后果:同时匹配两个 判别项 与匹配一个二元组并不等价。 同时匹配会保留判别项与模式之间的联系,使模式匹配不仅能细化局部上下文中假设的类型,也能细化 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 的期望类型。 本质上,Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 的精译规则会对判别项作特殊处理;因此,对判别项做出虽能保持程序运行时含义、却不一定保持编译时含义的改动,并不安全。

下面这个求两个自然数最小值的函数,是按其第一个参数做结构递归定义的:

def min' (n k : Nat) : Nat := match n, k with | 0, _ => 0 | _, 0 => 0 | n' + 1, k' + 1 => min' n' k' + 1 termination_by structural n

若把对两个参数的同时模式匹配改写为对一个二元组做匹配,终止性分析就会失败:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application min' n' k' def min' (n k : Nat) : Nat := match (n, k) with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' n' k' + 1 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    min' n' k'

这是因为在把递归调用与更小的实参值对应起来时,该分析只考虑对形参本身的直接模式匹配。 把判别项包进一个二元组会破坏这种联系。

成对值下的结构递归

下面这个求一对数中两个分量之最小值的函数,无法通过结构递归精译。

failed to infer structural recursion: Cannot use parameter nk: the type Nat × Nat does not have a `.brecOn` recursor def min' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by structural nk
failed to infer structural recursion:
Cannot use parameter nk:
  the type Nat × Nat does not have a `.brecOn` recursor

这是因为该形参的类型 Prod 并不是递归的。 因此,它的构造器没有可通过模式匹配暴露出来的递归参数。

不过,这个定义可以通过 良基递归 被接受:

def min' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by nk
结构递归与定义相等

尽管 countdown 的递归出现被应用到了一个并非递减参数真子项的项上,下列定义仍会被接受:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown (n' + 0) termination_by structural n

这是因为 n' + 0n' 定义相等,而后者是 n 的真子项。 由模式匹配产生的 子项 会通过 匹配泛化 的规则与 判别项 关联起来,而这些规则尊重定义相等。

countdown' 中,递归出现被应用到了 0 + n' 上;它与 n' 并不定义相等,因为自然数上的加法是按照第二个参数做结构递归的:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' (0 + n') def countdown' (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown' (0 + n') termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' (0 + n')

7.6.2.2. 互结构递归🔗

Lean 支持用结构递归来定义 互递归 函数。 互递归既可以通过 互递归块 引入,也可能来自 Lean.Parser.Term.letrec : termlet rec 表达式和 Lean.Parser.Command.declaration : commandwhere 代码块。 互结构递归的规则,会应用到由互递归组的精译步骤所得、经过提升后且实际上互相递归的一组定义上。 若互递归组中的每个函数都带有指明该函数递减实参的 termination_by structural 注解,那么这些定义就会按结构递归来翻译。

此时,对递减实参的要求会扩展为:

  • 所有递减实参的类型都必须来自同一个归纳类型,或者更一般地,来自同一个互归纳类型组

  • 递减参数类型中的参数,对所有函数都必须相同,且只能依赖于函数实参的共同固定前缀。

这些函数不必与互归纳类型一一对应。 多个函数可以拥有同一类型的递减实参,而与该递减实参互递归的类型也不必全都对应到某个函数。

非互递归类型上的互结构递归

下面这个例子展示了在一个非互递归的归纳数据类型上进行互递归:

mutual def even : Nat Prop | 0 => True | n+1 => odd n termination_by structural n => n def odd : Nat Prop | 0 => False | n+1 => even n termination_by structural n => n end
互归纳类型上的互结构递归

下面这个例子展示了在互归纳类型上的递归。 函数 Exp.sizeApp.size 互相递归。

mutual inductive Exp where | var : String Exp | app : App Exp inductive App where | fn : String App | app : App Exp App end mutual def Exp.size : Exp Nat | .var _ => 1 | .app a => a.size termination_by structural e => e def App.size : App Nat | .fn _ => 1 | .app a e => a.size + e.size + 1 termination_by structural a => a end

App.numArgs 的定义是在类型 App 上做结构递归。 它说明互递归组中的归纳类型不必全部都参与处理。

def App.numArgs : App Nat | .fn _ => 0 | .app a _ => a.numArgs + 1 termination_by structural a => a

7.6.2.3. 推断结构递归🔗

若递归或互递归函数定义中没有 termination_by 子句,Lean 就会尝试推断一个合适的结构递减实参;做法实际上是按顺序尝试所有合适的形参。 若这一步搜索失败,Lean 随后会尝试推断 良基递归

对互递归函数而言,会尝试形参的各种组合,但会设置上限以避免组合爆炸。 如果只有部分互递归函数带有 termination_by structural 子句,那么对这些函数只考虑所指定的形参;而对其余函数,则会把所有形参都作为结构递归候选。

termination_by? 子句会显示推断出的终止性注解。 它还可以通过给出的建议或代码操作自动加入源文件。

推断出的终止性注解

Lean 会自动推断函数 half 是结构递归的。 termination_by? 子句会显示推断出的终止性注解,并且可以一键自动加入源文件。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 Try this: [apply] termination_by structural x => xtermination_by?
Try this:
  [apply] termination_by structural x => x

7.6.2.4. 使用按所有较小值递归的精译🔗

本节将更详细地说明精译结构递归函数时所使用的构造。 这种精译使用了由归纳类型递归器自动生成的 belowbrecOn 构造

递归与递归器

自然数加法可通过对第二个参数递归来定义。 这个函数显然是结构递归的。

def add (n : Nat) : Nat Nat | .zero => n | .succ k => .succ (add n k)

若使用 Nat.rec 定义,它就会远离大多数人习惯的记法。

def add' (n : Nat) := Nat.rec (motive := fun _ => Nat) n (fun Variable name `k` is not explicitly referenced. Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning: [apply] _k Note: This linter can be disabled with `set_option linter.unusedVariables false`k soFar => .succ soFar)

若结构递归调用所用的数据并非函数参数的直接子项,就需要发挥创意,或采用复杂但系统的编码。

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

理解这个函数的一种方式,是将它看作一种结构递归:每次调用都翻转一个位,并且仅在该位已设置时递增结果。

def helper : Nat Bool Nat := Nat.rec (motive := fun _ => Bool Nat) (fun _ => 0) (fun _ soFar => fun b => (if b then Nat.succ else id) (soFar !b)) def half' (n : Nat) : Nat := helper n false [0, 0, 1, 1, 2, 2, 3, 3, 4]#eval [0, 1, 2, 3, 4, 5, 6, 7, 8].map half'
[0, 0, 1, 1, 2, 2, 3, 3, 4]

无需发挥创意,可以改用一种称为所有较小值递归的通用技术。 所有较小值递归使用可针对每个归纳类型系统推导出的辅助定义;这些辅助定义以递归器来定义,Lean 会自动推导它们。 对于每个 Natn,类型 n.below (motive := mot) 为所有 k < n 提供一个类型为 mot k 的值,并将其表示为迭代的 依赖序对类型。 所有较小值递归器 Nat.brecOn 允许函数使用任意更小 Nat 值所对应的结果。 用它定义函数并不方便:

noncomputable def half'' (n : Nat) : Nat := Nat.brecOn n (motive := fun _ => Nat) fun k soFar => match k, soFar with | 0, _ | 1, _ => 0 | _ + 2, _, h, _ => h + 1

该函数被标记为 Lean.Parser.Command.declaration : commandnoncomputable,因为编译器不支持为所有较小值递归生成代码;这种递归旨在用于推理,而非生成高效代码。 不过,仍然可以使用内核测试该函数:

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

如有必要,half'' 函数体中的依赖模式匹配也可使用递归器(具体来说是 Nat.casesOn)来编码:

noncomputable def half''' (n : Nat) : Nat := n.brecOn (motive := fun _ => Nat) fun k => k.casesOn (motive := fun k' => (k'.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun k' => k'.casesOn (motive := fun k'' => (k''.succ.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun _ soFar => soFar.2.1.succ))

这个定义仍然有效。

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

然而,它现在已远离原始定义,而且变得难以为大多数人所理解。 递归器是出色的逻辑基础,却不是编写程序或证明的简便方式。

结构递归分析会尝试把递归 预定义 翻译成对相应结构递归构造的使用。 在这一步里,模式匹配已经被翻译成匹配器函数的调用;终止性检查器会对这些调用作特殊处理。 接着,它会对每一组参数尝试使用 brecOn 的翻译。

所有较小值递归表

此定义等价于 List.below

def List.below' {α : Type u} {motive : List α Sort u} : List α Sort (max (u + 1) u) | [] => PUnit | _ :: xs => motive xs ×' xs.below' (motive := motive)

换言之,对于给定的动机List.below' 是一个包含该动机在列表所有后缀上的实现的类型。

递归参数越多,就需要对积类型进行更深层的嵌套迭代。 例如,二叉树有两个递归出现。

inductive Tree (α : Type u) : Type u where | leaf | branch (left : Tree α) (val : α) (right : Tree α)

其对应的所有较小值递归表包含该动机在所有子树上的实现:

def Tree.below' {α : Type u} {motive : Tree α Sort u} : Tree α Sort (max (u + 1) u) | .leaf => PUnit | .branch left _val right => (motive left ×' left.below' (motive := motive)) ×' (motive right ×' right.below' (motive := motive))

对于列表和树,brecOn 运算符都只要求一个分支,而不是每个构造器各有一个分支。 该分支接收一个列表或树,以及所有较小值的结果表;它应据此满足所给值的动机。 对所给值进行依赖分情况分析会自动精化记忆表的类型,从而提供所需的一切。

以下定义分别等价于 List.brecOnTree.brecOn。 原始递归辅助函数 List.brecOnTableTree.brecOnTable 在计算最终结果的同时计算所有较小值递归表,而 brecOn 运算符的实际定义只是投影出结果。

def List.brecOnTable {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs ×' xs.below' (motive := motive) := match xs with | [] => step [] PUnit.unit, PUnit.unit | x :: xs => let res := xs.brecOnTable (motive := motive) step let val := step (x :: xs) res val, res def Tree.brecOnTable {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t ×' t.below' (motive := motive) := match t with | .leaf => step .leaf PUnit.unit, PUnit.unit | .branch left val right => let resLeft := left.brecOnTable (motive := motive) step let resRight := right.brecOnTable (motive := motive) step let branchRes := resLeft, resRight let val := step (.branch left val right) branchRes val, branchRes def List.brecOn' {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs := (xs.brecOnTable (motive := motive) step).1 def Tree.brecOn' {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t := (t.brecOnTable (motive := motive) step).1

below 构造把某个类型的每个值映射到“某个函数在所有更小值上的调用结果”;它可以理解为一张记忆化表,其中已经包含了所有更小值的结果。 below 构造中“更小值”的概念,与 真子项 的定义直接对应。

递归器要求为该归纳类型的每个构造器各提供一个实参;在 ι-归约 时,这些实参会以该构造器的参数(以及对递归参数递归后的结果)来调用。 而按所有较小值递归的算子 brecOn 只要求一个同时覆盖全部构造器的分支。 这个分支会收到一个值以及一张 below 表;该表包含对所有比给定值更小的值递归所得的结果,分支应利用表中的内容来满足这个给定值对应的动机。 若函数在某个给定参数(或参数组)上是结构递归的,那么所有递归调用的结果都已经出现在这张表里。

当递归函数的函数体被改写为对某个形参调用 brecOn 时,该形参与其“所有较小值表”都会进入作用域。 分析器会遍历函数体,寻找递归调用。 如果对这个形参做了匹配,那么它在局部上下文中的各次出现会先被泛化,再用模式实例化;“所有较小值表”的类型也同样如此。 通常,这种模式匹配会让“所有较小值表”的类型变得更具体,从而能够访问更小值对应的递归结果。 这种泛化过程实现了“模式是匹配判别项的 子项”这一规则。 当检测到函数的递归出现时,就会查询“所有较小值表”,看看其中是否含有所检查实参对应的结果。 若有,递归调用即可替换成从该表中的一次投影。 若没有,则说明这里所考虑的参数不支持结构递归。

精译过程示例

逐步考察 half 的精译时,第一步是手工把它反糖化成一个更简单的形式。 这并不完全符合 Lean 的实际处理方式,但当出现的 OfNat 实例更少时,输出会容易阅读得多。 这个较易读的定义:

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

可以改写成下面这个更底层一些的版本:

def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

精译器一开始会先精译出一个预定义,其中递归仍然保留,但除此之外,该定义已经落在 Lean 的核心类型论里。 开启编译器对预定义的追踪,并让美化打印更显式,就可以看到得到的预定义:

set_option trace.Elab.definition.body true in set_option pp.all true in [Elab.definition.body] half : Nat Nat := fun (x : Nat) => half.match_1.{1} (fun (x : Nat) => Nat) x (fun (_ : Unit) => Nat.zero) (fun (_ : Unit) => Nat.zero) fun (n : Nat) => Nat.succ (half n)def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

返回的跟踪消息是:

[Elab.definition.body] half : Nat → Nat :=
    fun (x : Nat) =>
      half.match_1.{1} (fun (x : Nat) => Nat) x
        (fun (_ : Unit) => Nat.zero)
        (fun (_ : Unit) => Nat.zero)
        fun (n : Nat) => Nat.succ (half n)

辅助匹配函数的定义是:

@[instance_reducible] def half.match_1.{u_1} : (motive : Nat Sort u_1) (x : Nat) (Unit motive Nat.zero) (Unit motive 1) ((n : Nat) motive n.succ.succ) motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n#print half.match_1
@[instance_reducible] def half.match_1.{u_1} : (motive : Nat  Sort u_1) 
  (x : Nat)  (Unit  motive Nat.zero)  (Unit  motive 1)  ((n : Nat)  motive n.succ.succ)  motive x :=
fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

把它排版得更易读一些,则为:

def half.match_1'.{u} : (motive : Nat Sort u) (x : Nat) (Unit motive Nat.zero) (Unit motive 1) ((n : Nat) motive n.succ.succ) motive x := fun Variable name `motive` is not explicitly referenced. Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning: [apply] _motive Note: This linter can be disabled with `set_option linter.unusedVariables false`motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

换言之,half 中使用的那组特定模式配置,被编码进了 half.match_1

这个定义是 half 预定义的一个更易读版本:

def half' : Nat Nat := fun (x : Nat) => half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- 0 的分支 (fun _ => 0) -- 1 的分支 (fun n => Nat.succ (half' n)) -- n + 2 的分支

要把它精译为一个结构递归函数,第一步是建立对 bRecOn 的调用。 该定义必须标记为 Lean.Parser.Command.declaration : commandnoncomputable,因为 Lean 不支持为 Nat.brecOn 这类递归器生成代码。

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => don't know how to synthesize placeholder context: x n:Nattable:Nat.below nNat_ /- 待翻译: half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- 0 的分支 (fun _ => 0) -- 1 的分支 (fun n => Nat.succ (half' n)) -- n + 2 的分支 -/

下一步是把原函数体中出现的 x 替换为 brecOn 提供的 n。 由于 table 的类型依赖于 x,因此在用 half.match_1 分情况时,它也必须一并泛化,从而得到一个带额外参数的动机。

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n don't know how to synthesize placeholder for argument `h_1` context: x n:Nattable:Nat.below nUnit Nat.below Nat.zero Nat_ don't know how to synthesize placeholder for argument `h_2` context: x n:Nattable:Nat.below nUnit Nat.below 1 Nat_ don't know how to synthesize placeholder for argument `h_3` context: x n:Nattable:Nat.below n(n : Nat) Nat.below n.succ.succ Nat_) table /- 待翻译: (fun _ => 0) -- 0 的分支 (fun _ => 0) -- 1 的分支 (fun n => Nat.succ (half' n)) -- n + 2 的分支 -/

这三个分支中的占位符分别需要如下类型:

don't know how to synthesize placeholder for argument `h_1`
context:
x n:Nattable:Nat.below nUnit  Nat.below Nat.zero  Nat
don't know how to synthesize placeholder for argument `h_2`
context:
x n:Nattable:Nat.below nUnit  Nat.below 1  Nat
don't know how to synthesize placeholder for argument `h_3`
context:
x n:Nattable:Nat.below n(n : Nat)  Nat.below n.succ.succ  Nat

预定义中的前两个分支都是常量函数,没有递归需要检查:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) don't know how to synthesize placeholder for argument `h_3` context: x n:Nattable:Nat.below n(n : Nat) Nat.below n.succ.succ Nat_) table /- 待翻译: (fun n => Nat.succ (half' n)) -- n + 2 的分支 -/

最后一个分支包含递归调用。 它应当被翻译为对“所有较小值表”的一次查找。 最后一个洞的类型,用更易读的形式写出来是:

(n : Nat) Nat.below (motive := fun _ => Nat) n.succ.succ Nat

它等价于

(n : Nat) Nat ×' (Nat ×' Nat.below (motive := fun _ => Nat) n) Nat

“所有较小值表”中的第一个 Nat,是对 n + 1 递归所得的结果;第二个则是对 n 递归所得的结果。 因此,递归调用可以替换成一次查找,于是精译成功:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) (fun _ table => Nat.succ table.2.1) table unexpected end of input; expected ')', ',' or ':'

实际的精译器会在动机中插入带新鲜名称的哨兵类型,以此跟踪“当前检查是否结构递归的参数”与“所有较小值表中的位置”之间的对应关系。

7.6.3. 良基递归🔗

良基递归 定义的函数,是指其中每次递归调用的实参都在某种适当意义下比函数形参更小的函数。 与结构递归不同,后者要求递归定义满足特定的句法要求,而良基递归的定义使用的是语义论证。 这使得更大一类递归定义能够被接受。 此外,当 Lean 的自动化无法构造终止性证明时,也可以手工给出。

Lean 编译器会以完全相同的方式对待所有这些定义。 在 Lean 的逻辑中,使用良基递归的定义通常不会 在定义上 归约。 不过,这些归约在命题相等层面仍然成立,而 Lean 会自动证明它们。 这通常不会让证明良基递归定义的性质变得更困难,因为可以利用这些命题性的归约来推理函数行为。 但这也意味着,这类函数通常不太适合出现在类型中。 即便其归约行为碰巧在定义上成立,它在内核中的速度通常仍比结构递归定义慢得多,因为内核必须连同定义一起展开终止性证明。 因此,只要可能,那些打算在类型中使用、或在其他依赖定义相等的重要场景中使用的递归函数,都应优先定义为结构递归。

若要显式使用良基递归,可以在函数或定理定义上添加 Lean.Parser.Command.declaration : commandtermination_by 子句,用来指定函数终止所依据的 度量。 该度量应是一个在每次递归调用时都会减小的项;它可以是函数的某个形参、若干形参组成的元组,也可以是任意其他项。 这个度量的类型必须配备一个 良基关系,它决定了“度量减小”究竟意味着什么。

语法显式良基递归

Lean.Parser.Command.declaration : commandtermination_by 子句用来引入终止性论证。

Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferred termination measure will be suggested.

terminationBy ::= ...
    | Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferred termination measure will be suggested.

termination_by (ident* =>)? term

可选 => 之前的标识符可以把尚未在声明头中绑定的函数形参带入作用域,而后面必需的项必须指明函数的某个形参,无论它是在声明头中引入,还是在该子句中局部引入。

通过反复减法定义除法

除法可以刻画为“除数能从被除数中减去多少次”。 这个操作不能用结构递归来精译,因为减法不是模式匹配。 不过 n 的值确实会在每次递归调用时减小,因此可以用良基递归来为这种“反复减法求除法”的定义提供正当性。

def div (n k : Nat) : Nat := if k = 0 then 0 else if k > n then 0 else 1 + div (n - k) k termination_by n

7.6.3.1. 良基关系🔗

若不存在无限下降链,则关系 是一个 良基关系

x_0 ≻ x_1 ≻ \cdots

在 Lean 中,凡是带有规范良基关系的类型,都是类型类 WellFoundedRelation 的实例。

🔗类型类
WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)
WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)

具有规范良基关系的类型。

实例用于证明以良基递归定义的函数会终止:递归调用必须使某个度量按照良基关系减小。 这个关系可以组合递归函数各形参上的良基关系。

WellFoundedRelation.mk.{u}
rel : α  α  Prop

α 上的一个良基关系。

wf : WellFounded WellFoundedRelation.rel

rel 确实良基的证明。

最重要的实例有:

  • Nat,按 (· < ·) 排序。

  • Prod,按字典序排序:当且仅当 a₁ a₂,或 a₁ = a₂b₁ b₂ 时,有 (a₁, b₁) (a₂, b₂)

  • 每个属于类型类 SizeOf(其提供方法 SizeOf.sizeOf)的类型,都带有一个良基关系。 对这些类型,x₁ x₂ 当且仅当 sizeOf x₁ < sizeOf x₂。对于 归纳类型,Lean 会自动派生出 SizeOf 实例。

注意,存在一个低优先级实例 instSizeOfDefault,它会为任意类型提供一个 SizeOf 实例,并且总是返回 0。 这个实例不能用来借助良基递归证明函数终止,因为 0 < 0 为假。

默认的 Size 实例

函数类型一般并没有对终止性证明有用的良基关系。 因此,实例合成会选中 instSizeOfDefault 及其对应的良基关系。 如果度量本身是一个函数,那么就会选中默认的 SizeOf 实例,证明也就不可能成功。

declaration uses `sorry`def declaration uses `sorry`fooInst (b : Bool Bool) : Unit := fooInst (b b) termination_by b decreasing_by b:Bool BoolsizeOf (b b) < sizeOf b b:Bool Bool0 < 0 b:Bool Bool0 < 0 b:Bool BoolFalse b:Bool BoolFalse All goals completed! 🐙

7.6.3.2. 终止性证明🔗

一旦指定了 度量 并确定了其 良基关系,Lean 就会为每个递归调用生成终止性证明目标。

每个递归调用对应的证明目标都形如 g a₁ a₂ g p₁ p₂ ,其中:

  • g 是把形参映射到度量值的函数;

  • 是推断出来的良基关系;

  • a₁ a₂ 是递归调用的实参;

  • p₁ p₂ 是函数定义的形参。

证明目标的上下文,就是该递归调用所在的局部上下文。 尤其是,局部假设(例如由 if h : _match h : _ with have 引入的那些)都是可用的。 如果函数的某个形参是某次模式匹配(例如通过 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式)的 判别项,那么在证明目标中,这个形参会被细化为与之匹配的模式。

整体的终止性证明目标由若干个子目标组成,每个递归调用对应一个子目标。 默认情况下,会使用策略 decreasing_trivial 来证明每个证明目标。 也可以在 Lean.Parser.Command.declaration : commandtermination_by 子句之后,通过可选的 Lean.Parser.Command.declaration : commanddecreasing_by 子句提供自定义策略脚本。 该策略脚本只会运行一次;运行时同时拥有每个证明目标对应的一个子目标,而不是对每个证明目标分别运行。

终止性证明目标

下面这个 Fibonacci 数的递归定义有两个递归调用,因此终止性证明中会产生两个目标。

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n:Nath:¬n 1n - 1 < n n:Nath:¬n 1n - 2 < ndecreasing_by n:Nath:¬n 1n - 1 < nn:Nath:¬n 1n - 2 < n
n:Nath:¬n 1n - 1 < nn:Nath:¬n 1n - 2 < n

这里的 度量 就是参数本身,而良基顺序则是自然数上的小于关系。 第一个证明目标要求用户证明:第一次递归调用的实参,也就是 n - 1,严格小于函数的形参 n

这两个终止性证明都可以很容易地用 omega 策略解决。

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n decreasing_by n:Nath:¬n 1n - 1 < n All goals completed! 🐙 n:Nath:¬n 1n - 2 < n All goals completed! 🐙
细化后的参数

如果函数的某个参数是某次模式匹配的 判别项,那么证明目标中会出现细化后的参数。

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib (n + 1) + fib n termination_by n => n unsolved goals n:Natn + 1 < n.succ.succ n:Natn < n.succ.succdecreasing_by n:Natn + 1 < n.succ.succn:Natn < n.succ.succ
n:Natn + 1 < n.succ.succn:Natn < n.succ.succ

此外,上下文还会被补充进一些额外假设,以便更容易证明终止性。 例如:

  • if-then-else 表达式的各个分支中,会加入一个断言当前分支条件成立的假设,效果类似于使用依赖式 if-then-else 语法。

  • 在某些高阶函数的函数实参中,函数体的上下文会被补充进关于该实参的假设。

这个列表并不穷尽,而且该机制是可扩展的。 其详细说明见预处理一节

增强后的证明目标上下文

这里,termIfThenElse : term`if c then t else e` 是 `ite c t e`(即“如果—那么—否则”)的记法;它根据 `c` 是否为真返回 `t` 或 `e`。 显式参数 `c : Prop` 本身没有计算内容;另有一个由实例合成得到的 `[Decidable c]` 参数,真正决定如何把 `c` 求值为真或假。 写成 `if h : c then t else e` 时表示依赖式条件 `dite`,此时 `t` 和 `e` 可以使用 `c` 为真或假的事实。 标识符中的记法约定:建议将 `if c then t else e` 写作 `ite`,并分别用 `left`、`right` 指代 `t`、`e`。if 并不会把关于条件(也就是 n 1 是否成立)的局部假设加入各分支的局部上下文中。

def fib (n : Nat) := if n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n:Nath✝:¬n 1n - 1 < n n:Nath✝:¬n 1n - 2 < ndecreasing_by n:Nath✝:¬n 1n - 1 < nn:Nath✝:¬n 1n - 2 < n

不过,在终止性证明的上下文中,这些假设仍然可用:

n:Nath✝:¬n 1n - 1 < nn:Nath✝:¬n 1n - 2 < n

位于 Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates over the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `Std.ToStream` typeclass. forLean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates over the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `Std.ToStream` typeclass. in 循环体内的终止性证明目标也会被增强;这里增强进来的是一个关于 Std.Legacy.Range 的成员资格假设:

def f (xs : Array Nat) : Nat := Id.run do let mut s := xs.sum for i in [:xs.size] do s := s + f (xs.take i) pure s termination_by xs unsolved goals xs:Array Nats:Nat := xs.sumi:Nath✝:i [:xs.size]sizeOf (xs.take i) < sizeOf xsdecreasing_by xs:Array Nats:Nat := xs.sumi:Nath✝:i [:xs.size]sizeOf (xs.take i) < sizeOf xs
xs:Array Nati:Nath✝:i [:xs.size]sizeOf (xs.take i) < sizeOf xs

类似地,在下列这个(刻意构造的)例子中,终止性证明会额外带上一个说明 x xs 的假设。

def f (n : Nat) (xs : List Nat) : Nat := List.sum (xs.map (fun x => f x [])) termination_by xs unsolved goals n:Natxs:List Natx:Nath✝:x xssizeOf [] < sizeOf xsdecreasing_by n:Natxs:List Natx:Nath✝:x xssizeOf [] < sizeOf xs
n:Natxs:List Natx:Nath✝:x xssizeOf [] < sizeOf xs

这一特性要求为递归调用所嵌套其下的高阶函数进行特殊设置,详见预处理一节。 下面这个定义除了用一个自定义的等价函数替代 List.map 之外,与上面完全相同;此时证明目标的上下文就不会被增强:

def List.myMap := @List.map def f (n : Nat) (xs : List Nat) : Nat := List.sum (xs.myMap (fun x => f x [])) termination_by xs unsolved goals n:Natxs:List Natx:NatsizeOf [] < sizeOf xsdecreasing_by n:Natxs:List Natx:NatsizeOf [] < sizeOf xs
n:Natxs:List Natx:NatsizeOf [] < sizeOf xs

7.6.3.3. 默认终止性证明策略🔗

如果没有给出 Lean.Parser.Command.declaration : commanddecreasing_by 子句,那么会隐式使用 decreasing_tactic,并将其分别应用到每个证明目标上。

🔗tactic
decreasing_tactic

decreasing_tactic 主要处理元组的字典序:如果积类型左分量 定义相等,它就应用 Prod.Lex.right;否则应用 Prod.Lex.left。 按这种方式预处理完元组之后,它会调用 decreasing_trivial 策略。

🔗tactic
decreasing_trivial

decreasing_trivial 是一个可扩展的策略,它会应用若干常见启发式来解决终止性目标。 具体来说,它会尝试下列策略与定理:

  • simp_arith

  • assumption

  • 定理 Nat.sub_succ_lt_selfNat.pred_lt_of_ltNat.pred_lt,用来处理常见的算术目标

  • omega

  • array_get_decarray_mem_dec,用于证明数组元素的大小小于数组本身的大小

  • sizeOf_list_dec,用于证明列表元素的大小小于列表本身的大小

  • String.Legacy.Iterator.sizeOf_next_lt_of_hasNextString.Legacy.Iterator.sizeOf_next_lt_of_atEnd,用于处理借助 Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates over the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `Std.ToStream` typeclass. for 遍历字符串的情形

这个策略旨在通过 Lean.Parser.Command.macro_rules : commandmacro_rules 继续扩展出更多启发式。

字典序不回溯

需要更复杂 度量 的递归函数,一个经典例子就是 Ackermann 函数:

def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) termination_by m n => (m, n)

该度量是一个元组,因此每个递归调用的实参都必须在字典序意义下小于函数形参。 默认的 decreasing_tactic 可以处理这种情况。

特别要注意,第三个递归调用的第二个实参小于第二个形参,而第一个实参与第一个形参在定义上相等。 这使得 decreasing_tactic 可以应用 Prod.Lex.right

Prod.Lex.right {α β} {ra : α α Prop} {rb : β β Prop} (a : α) {b₁ b₂ : β} (h : rb b₁ b₂) : Prod.Lex ra rb (a, b₁) (a, b₂)

然而,若把函数定义改成下面这样,它就会失败:第三个递归调用的第一个实参虽然可证明小于或等于第一个形参,但二者在句法上并不相等:

def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal m n:Natm / 2 + 1 < m + 1synack (m / 2 + 1) n) termination_by m n => (m, n)
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
m n:Natm / 2 + 1 < m + 1

由于 Prod.Lex.right 不适用,该策略就改用了 Prod.Lex.left,从而产生了上面那个无法证明的目标。

这个函数定义可能需要手工证明,并使用更一般的定理 Prod.Lex.right';该定理允许元组的第一个分量(其类型必须是 Nat)只需小于或等于,而不必严格相等:

Prod.Lex.right' {β} (rb : β β Prop) {a₂ : Nat} {b₂ : β} {a₁ : Nat} {b₁ : β} (h₁ : a₁ a₂) (h₂ : rb b₁ b₂) : Prod.Lex Nat.lt rb (a₁, b₁) (a₂, b₂)def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (synack (m / 2 + 1) n) termination_by m n => (m, n) decreasing_by m:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, 1) (m.succ, 0) m:Natm < m.succ All goals completed! 🐙 -- 下一个目标对应第三个递归调用 m:Natn:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m / 2 + 1, n) (m.succ, n.succ) m:Natn:Natm / 2 + 1 m.succm:Natn:Natn < n.succ m:Natn:Natm / 2 + 1 m.succ All goals completed! 🐙 m:Natn:Natn < n.succ All goals completed! 🐙 m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y m.succ, n.succ NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, x✝ m / 2 + 1, n ) (m.succ, n.succ) m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y m.succ, n.succ Natm < m.succ All goals completed! 🐙

decreasing_tactic 不使用更强的 Prod.Lex.right',因为那样一来在失败时就需要回溯。

7.6.3.4. 推断良基递归🔗

如果递归函数定义没有指明终止性 度量,Lean 就会尝试自动发现一个。 如果既没有提供 Lean.Parser.Command.declaration : commandtermination_by,也没有提供 Lean.Parser.Command.declaration : commanddecreasing_by,Lean 会先尝试推断结构递归,再尝试良基递归。 如果存在 Lean.Parser.Command.declaration : commanddecreasing_by 子句,则只会尝试良基递归。

为了推断一个合适的终止性 度量,Lean 会考虑多个 基础终止度量——即类型为 Nat 的终止度量——然后尝试这些度量的所有元组组合。

所考虑的基础终止度量有:

  • 所有类型带有非平凡 SizeOf 实例的形参

  • 表达式 e₂ - e₁:前提是某个递归调用的局部上下文中有一个类型为 e₁ < e₂e₁ ≤ e₂ 的假设,其中 e₁e₂ 的类型都是 Nat,且只依赖于函数形参。 这种方法基于 Panagiotis Manolios and Daron Vroon, 2006. “Termination Analysis with Calling Context Graphs”. In Proceedings of the International Conference on Computer Aided Verification (CAV 2006). (LNCS 4144) 的工作。

  • 在互递归组中,还会使用一个额外的基础度量,以区分“对组内其他函数的递归调用”和“对当前正在定义函数本身的递归调用”(详见互良基递归一节

候选度量 是基础度量或基础度量的元组。 如果某个候选度量能让终止性证明策略消去所有证明目标(即由 Lean.Parser.Command.declaration : commanddecreasing_by 指定的策略;若没有 Lean.Parser.Command.declaration : commanddecreasing_by 子句,则为 decreasing_trivial),那么系统就会从中任意选择一个作为自动终止度量。

termination_by? 子句会显示推断出的终止性注解。 它还可以通过给出的建议或代码操作自动加入源文件。

为了避免尝试所有度量元组所带来的组合爆炸,Lean 会先把所有 基础终止度量 制成表格,判断每个基础度量是“递减”“严格递减”还是“非递减”。 所谓递减度量,是指它在至少一个递归调用处变小,且在任何递归调用处都不会增大;所谓严格递减度量,则是在所有递归调用处都变小。 非递减度量则是指终止性策略无法证明其递减或严格递减。 随后会根据这张表来选取合适的元组。这种方法基于 Lukas Bulwahn, Alexander Krauss, and Tobias Nipkow, 2007. “Finding Lexicographic Orders for Termination Proofs in Isabelle/HOL”. In Proceedings of the International Conference on Theorem Proving in Higher Order Logics (TPHOLS 2007). (LNTCS 4732) 当找不到自动度量时,这张表会显示在错误消息中。

终止性失败

如果没有 Lean.Parser.Command.declaration : commandtermination_by 子句,Lean 会尝试推断良基递归的度量。 如果推断失败,它就会打印上文所述的表格。 在此示例中,Lean.Parser.Command.declaration : commanddecreasing_by 子句只是阻止 Lean 同时尝试结构递归,从而让错误消息保持针对性。

Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) n m l 1) 32:6-25 = = = 2) 33:6-23 = < _ 3) 34:6-23 < _ _ Please use `termination_by` to specify a decreasing measure.def f : (n m l : Nat) Nat | n+1, m+1, l+1 => [ f (n+1) (m+1) (l+1), f (n+1) (m-1) (l), f (n) (m+1) (l) ].sum | _, _, _ => 0 decreasing_by all_goals decreasing_tactic
Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
           n m l
1) 32:6-25 = = =
2) 33:6-23 = < _
3) 34:6-23 < _ _
Please use `termination_by` to specify a decreasing measure.

这三个递归调用通过其源码位置来标识。 这条消息表达了以下事实:

  • 在第一次递归调用中,所有参数都(可证明地)等于对应的形参

  • 在第二次递归调用中,第一个参数等于第一个形参,且第二个参数可证明地小于第二个形参。 此递归调用没有检查第三个参数,因为要判定不存在合适的终止参数,并不需要检查它。

  • 在第三次递归调用中,第一个参数严格减小,其他参数则未被检查。

当终止性证明以这种方式失败时,发现问题的一种好方法是使用 Lean.Parser.Command.declaration : commandtermination_by 明确指出预期的终止参数。 这样会显示失败策略所产生的消息。

数组索引

e₂ - e₁ 形式的表达式纳入度量候选,目的是支持一种常见写法:向某个上界递增计数,尤其是以各种有趣方式遍历数组时。 在下面这个对有序数组进行二分查找的函数中,这个启发式帮助 Lean 找到了 j - i 这一度量。

def binarySearch (x : Int) (xs : Array Int) : Option Nat := go 0 xs.size where go (i j : Nat) (hj : j xs.size := by omega) := if h : i < j then let mid := (i + j) / 2 let y := xs[mid] if x = y then some mid else if x < y then go i mid else go (mid + 1) j else none Try this: [apply] termination_by (j, j - i)termination_by?

从推断出的度量里包含一个冗余的 j 可以看出:推断出的终止性论证使用的是某个可行但任意的度量,而不是最优或最简的度量:

Try this:
  [apply] termination_by (j, j - i)
推断期间的终止性证明策略

Lean.Parser.Command.declaration : commanddecreasing_by 指定的策略,在推断终止性 度量 时与在实际终止性证明中使用时略有不同。

  • 在推断期间,它只会应用于单个目标,尝试证明 <Nat 上成立。

  • 在终止性证明期间,它会面对多个同时存在的目标(每个递归调用一个),且这些目标可能涉及二元组的字典序。

因此,某个 Lean.Parser.Command.declaration : commanddecreasing_by 代码块即便在显式给出终止性论证时能够逐个解决目标,也可能导致终止度量的推断失败:

Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) x1 x2 1) 638:16-23 ? ? 2) 639:27-40 _ _ 3) 639:20-41 _ _ Please use `termination_by` to specify a decreasing measure.def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) decreasing_by · apply Prod.Lex.left omega · apply Prod.Lex.right omega · apply Prod.Lex.left omega

因此,只要写了显式的 Lean.Parser.Command.declaration : commanddecreasing_by 证明,通常都建议同时写上 Lean.Parser.Command.declaration : commandtermination_by 子句。

推断过于强大

由于 decreasing_tactic 在字典序方面并不完备,以此避免回溯,Lean 可能会推断出某个终止性 度量,但由此产生的目标却是该策略本身无法证明的。 此时,错误消息反映的是 策略证明失败,而不是“无法找到度量”。 notAck 中发生的正是这种情况:

def notAck : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => notAck m 1 | m + 1, n + 1 => notAck m (notAck (m / 2 + 1) n) decreasing_by all_goals failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal m n:Natm / 2 + 1 < m + 1All goals completed! 🐙
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
m n:Natm / 2 + 1 < m + 1

在这种情况下,显式写出终止性 度量 会有帮助。

7.6.3.5. 互良基递归🔗

Lean 支持用 良基递归 来定义 互递归 函数。 互递归既可以通过 互递归块 引入,也可能来自 Lean.Parser.Term.letrec : termlet rec 表达式和 Lean.Parser.Command.declaration : commandwhere 代码块。 互良基递归的规则,会应用到由互递归组的精译步骤所得、经过提升后且实际上互相递归的一组定义上。

如果互递归组中的任意一个函数带有 Lean.Parser.Command.declaration : commandtermination_byLean.Parser.Command.declaration : commanddecreasing_by 子句,就会尝试良基递归。 如果互递归组中任意一个函数通过 Lean.Parser.Command.declaration : commandtermination_by 指定了终止性 度量,那么组内所有函数都必须指定终止度量,而且这些度量必须具有相同的类型。

如果没有指定终止性论证,则会像上文所述那样自动推断。在互递归的情况下,推断时还会考虑第三类基础度量:对互递归组中的每个函数,各自有一个在该函数上取 1、在其他函数上取 0 的度量。这使得 Lean 能对这些函数本身排序,从而允许某些“从一个函数调用另一个函数”的情况,即使形参并未减小。

参数不下降的互递归

在下面这组互递归函数定义中,从 g 调用 f 时参数并没有减小。 尽管如此,由于额外的基础度量对函数本身施加了顺序,这个定义仍会被接受。

mutual def f : (n : Nat) Nat | 0 => 0 | n + 1 => g n Try this: [apply] termination_by n => (n, 0)termination_by? def g (n : Nat) : Nat := (f n) + 1 Try this: [apply] termination_by (n, 1)termination_by? end

f 推断出的终止性论证是:

Try this:
  [apply] termination_by n => (n, 0)

g 推断出的终止性论证是:

Try this:
  [apply] termination_by (n, 1)

7.6.3.6. 函数定义的预处理🔗

在确定每个调用点的证明目标之前,Lean 会先对函数体做预处理,把它变换成一个等价但可能携带附加信息的定义。 这个预处理步骤主要用于向局部上下文补充求解终止性证明目标所必需的额外假设,从而免去用户手工做等价变换。 预处理会使用化简器,并且用户可以扩展它。

预处理分三步进行:

  1. Lean 会用 wfParam 小工具 标注函数形参,或形参某个子项的各次出现。

    wfParam {α} (a : α) : α

    更精确地说,函数形参的每次出现都会被包上一层 wfParam。 只要某个 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式有任意一个判别项被 wfParam 包裹,这个小工具就会被移除,并且所有模式匹配变量的每次出现(无论它是否来自那个带有 wfParam 小工具的判别项)都会改为包上一层 wfParam。 此外,wfParam 小工具还会从 投影函数 应用中被上浮出来。

  2. 带标注的函数体会使用化简器进行化简,并且只使用来自 wf_preprocess 自定义 simp 集 的化简规则。

  3. 最后,移除所有残留的 wfParam 标记。

对用于良基递归的函数形参进行这种标注,可以让预处理的化简规则区分“形参”和“其他项”。

属性良基递归的预处理 Simp 集
attr ::= ...
    | Theorems tagged with the `wf_preprocess` attribute are used during the processing of functions defined
by well-founded recursion. They are applied to the function's body to add additional hypotheses,
such as replacing `if c then _ else _` with `if h : c then _ else _` or `xs.map` with
`xs.attach.map`. Also see `wfParam`.

Warning: These rewrites are only applied to the declaration for the purpose of the logical
definition, but do not affect the compiled code. In particular they can cause a function definition
that diverges as compiled to be accepted without an explicit `partial` keyword, for example if they
remove irrelevant subterms or change the evaluation order by hiding terms under binders. Therefore
avoid tagging theorems with `[wf_preprocess]` unless they preserve also operational behavior.
wf_preprocess

带有 wf_preprocess 属性的定理,会在处理以良基递归定义的函数时使用。它们会被应用于 函数体,以加入额外假设,例如把 if c then _ else _ 替换为 if h : c then _ else _,或把 xs.map 替换为 xs.attach.map。另见 wfParam

警告:这些重写只会为了构造逻辑定义而应用于声明,并不影响编译后的代码。尤其是, 如果重写删除无关子项,或把项隐藏在绑定变量之下而改变求值顺序,就可能使一个编译后 发散的函数在没有显式 partial 关键字时仍被接受。因此,除非定理同时保持运行时行为, 否则应避免给它添加 [wf_preprocess] 标记。

🔗定义
wfParam.{u} {α : Sort u} (a : α) : α
wfParam.{u} {α : Sort u} (a : α) : α

wfParam 小工具在通过良基递归构造递归函数时供内部使用;它用于跟踪哪个形参适合由 系统自动引入 List.attach(或类似操作)。

wf_preprocess simp 集中的某些重写规则会无条件地一般适用,而不理会 wfParam 标记。 特别地,定理 ite_eq_dite 会被用来扩展 if-then-else 表达式各分支的上下文,在其中加入关于条件的一个假设:这个假设的名字应当是一个基于 h 的不可访问名;这一点可由对项 () 使用 binderNameHint 看出。绑定变量名提示见策略语言参考

ite_eq_dite {P : Prop} {α : Sort u} {a b : α} [Decidable P] : (if P then a else b) = if h : P then binderNameHint h () a else binderNameHint h () b

其他重写规则则会利用 wfParam 标记来限制自身的适用范围;它们只在某个函数(例如 List.map)作用于一个形参或其子项时才会使用,否则不会。 这通常分两步完成:

  1. List.map_wfParam 这样的定理,会识别 List.map 作用于函数形参(或其子项)的调用,并借助 List.attach 用“它们确实是该列表元素”这一断言来丰富列表元素的类型:

    List.map_wfParam (xs : List α) (f : α β) : (wfParam xs).map f = xs.attach.unattach.map f
  2. List.map_unattach 这样的定理,会让这个断言对 List.map 的函数参数可用。

    List.map_unattach (P : α Prop) (xs : List { x : α // P x }) (f : α β) : xs.unattach.map f = xs.map fun x, h => binderNameHint x f <| binderNameHint h () <| f (wfParam x)

    如果 f 是一个 lambda 表达式,这个定理会使用 binderNameHint 小工具来保留用户选择的绑定变量名。

通过把 List.attach 的引入与所引入假设的传播分离开来,即使是在 (xs.reverse.filter p).map f 这样的链式调用中,也能把期望的 x xs 假设提供给 f

可以通过把选项 wf.preprocess 设为 false 来关闭这一预处理。 若想查看预处理后的函数定义(包括移除 wfParam 标记之前和之后的版本),可将选项 trace.Elab.definition.wf 设为 true

🔗选项
trace.Elab.definition.wf

默认值:false

启用或禁用指定模块及其子模块的追踪。对 trace.Elab.definition.wf 而言,启用后会显示 良基递归精译过程的诊断信息。

默认值为 false

自定义数据类型的预处理

此示例演示了要为自定义容器类型启用自动良基递归,需要具备哪些内容。 结构类型 Pair 是同质序对:它恰好包含两个类型相同的元素。 可以把它看作一种总是恰好包含两个元素的列表或数组。

作为容器,Pair 可以支持 map 操作。 为了支持递归调用出现在映射到 Pair 上的函数体内的良基递归,需要一些额外定义,包括成员关系谓词、关联成员大小与包含该成员的序对大小的定理、引入和消去成员关系假设的辅助函数、用于插入这些辅助函数的 wf_preprocess 规则,以及对 decreasing_trivial 策略的扩展。 这些步骤都会使 Pair 更易使用,但没有哪一步是严格必需的;不必立即为每种类型实现所有步骤。

/-- 同质序对 -/ structure Pair (α : Type u) where fst : α snd : α /-- 将函数映射到序对的元素上 -/ def Pair.map (f : α β) (p : Pair α) : Pair β where fst := f p.fst snd := f p.snd

定义一个使用 Pair 的二叉树嵌套归纳数据类型,并尝试定义其 map 函数,可以说明预处理规则的必要性。

/-- 使用 `Pair` 定义的二叉树 -/ inductive Tree (α : Type u) where | leaf : α Tree α | node : Pair (Tree α) Tree α

直接定义 map 函数会失败:

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal α:Type u_1p:Pair (Tree α)t':Tree αsizeOf t' < 1 + sizeOf pt'.map f)) termination_by t => t
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
α:Type u_1p:Pair (Tree α)t':Tree αsizeOf t' < 1 + sizeOf p

这个证明义务显然无法解决,因为没有任何信息将 t'p 联系起来。

启用这类函数定义的标准惯用法,是使用一个函数为集合中的每个元素附上其确实属于该集合的证明。 陈述这一性质需要成员关系谓词。

inductive Pair.Mem (p : Pair α) : α Prop where | fst : Mem p p.fst | snd : Mem p p.snd instance : Membership α (Pair α) where mem := Pair.Mem

每个归纳类型都会自动拥有一个 SizeOf 实例。 集合中的元素应当小于该集合,但必须先证明这一事实,才能用它构造终止性证明:

theorem Pair.sizeOf_lt_of_mem {α} [SizeOf α] {p : Pair α} {x : α} (h : x p) : sizeOf x < sizeOf p := α:Type u_1inst✝:SizeOf αp:Pair αx:αh:x psizeOf x < sizeOf p α:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.fst < sizeOf { fst := fst✝, snd := snd✝ }α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } (α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α0 < 1 + sizeOf fst✝; All goals completed! 🐙)

下一步是定义 attachunattach 函数:前者为序对中的元素附上其属于该序对的证明,后者则移除该证明。 这里,Pair.unattach 的类型更为一般,可用于任意子类型;这是一种典型模式。

def Pair.attach (p : Pair α) : Pair {x : α // x p} where fst := p.fst, .fst snd := p.snd, .snd def Pair.unattach {P : α Prop} : Pair {x : α // P x} Pair α := Pair.map Subtype.val

现在可以通过显式使用 Pair.attachPair.sizeOf_lt_of_mem 来定义 Tree.map

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.attach.map (fun t', _ => t'.map f)) termination_by t => t decreasing_by α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' pthis:sizeOf t' < sizeOf psizeOf t' < sizeOf (node p) α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' pthis:sizeOf t' < sizeOf psizeOf t' sizeOf p All goals completed! 🐙

这一变换可以完全自动化。 可以使用良基递归的预处理功能,自动引入 Pair.attach 函数。 这分两个阶段完成。 首先,当 Pair.map 应用于函数的某个形参时,将其重写为 attach/unattach 组合。 然后,当一个函数被映射到 Pair.unattach 的结果上时,将该函数重写为接收成员关系证明,并把该证明引入作用域。

@[wf_preprocess] theorem Pair.map_wfParam (f : α β) (p : Pair α) : (wfParam p).map f = p.attach.unattach.map f := α:Type u_1β:Type u_2f:α βp:Pair αmap f (wfParam p) = map f p.attach.unattach α:Type u_1β:Type u_2f:α βfst✝:αsnd✝:αmap f (wfParam { fst := fst✝, snd := snd✝ }) = map f { fst := fst✝, snd := snd✝ }.attach.unattach All goals completed! 🐙 @[wf_preprocess] theorem Pair.map_unattach {P : α Prop} (p : Pair (Subtype P)) (f : α β) : p.unattach.map f = p.map fun x, Variable name `h` is not explicitly referenced. Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning: [apply] _h Note: This linter can be disabled with `set_option linter.unusedVariables false`h => binderNameHint x f <| f (wfParam x) := α:Type u_1β:Type u_2P:α Propp:Pair (Subtype P)f:α βmap f p.unattach = map (fun x => match x with | x, h => binderNameHint x f (f (wfParam x))) p α:Type u_1β:Type u_2P:α Propf:α βfst✝:Subtype Psnd✝:Subtype Pmap f { fst := fst✝, snd := snd✝ }.unattach = map (fun x => match x with | x, h => binderNameHint x f (f (wfParam x))) { fst := fst✝, snd := snd✝ }; All goals completed! 🐙

现在编写函数体时无需额外考虑,而终止性证明仍可使用成员关系假设。

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => t'.map f)) termination_by t => t decreasing_by α:Type u_1p:Pair (Tree α)t':Tree αh:t' pthis:sizeOf t' < sizeOf psizeOf t' < sizeOf (node p) α:Type u_1p:Pair (Tree α)t':Tree αh:t' pthis:sizeOf t' < sizeOf psizeOf t' < 1 + sizeOf p All goals completed! 🐙

可以仿照类似的内置定理,将 sizeOf_lt_of_mem 添加到 decreasing_trivial 策略中,使证明完全自动化。

macro "sizeOf_pair_dec" : tactic => `(tactic| with_reducible have := Pair.sizeOf_lt_of_mem _ omega done) macro_rules | `(tactic| decreasing_trivial) => `(tactic| sizeOf_pair_dec) def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => t'.map f)) termination_by t => t

为保持示例简短,sizeOf_pair_dec 策略专门适配了这一特定递归模式,并不足以泛用于通用容器库。 不过,它确实说明了库在实践中可以和标准库中的容器类型一样方便。

7.6.3.7. 理论与构造🔗

本节极其简要地介绍一下通过 良基递归 给出终止性证明所依赖的数学构造;这些构造偶尔会显露到表面。 良基递归函数的精译建立在算子 WellFounded.fix 之上。

🔗定义
WellFounded.fix.{u, v} {α : Sort u} {C : α Sort v} {r : α α Prop} (hwf : WellFounded r) (F : (x : α) ((y : α) r y x C y) C x) (x : α) : C x
WellFounded.fix.{u, v} {α : Sort u} {C : α Sort v} {r : α α Prop} (hwf : WellFounded r) (F : (x : α) ((y : α) r y x C y) C x) (x : α) : C x

良基不动点。若对某个值,假设所有按良基关系小于它的值都满足动机 C,便足以推出 当前值也满足 C,那么所有值都满足 C

此函数用于良基递归的精译过程。

类型 α 会实例化为函数的(会变化的)形参,并用 PSigma 将它们打包成一个类型。 WellFounded 关系则通过 invImage 由终止性 度量 构造出来。

🔗定义
invImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α β) (h : WellFoundedRelation β) : WellFoundedRelation α
invImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α β) (h : WellFoundedRelation β) : WellFoundedRelation α

良基关系的逆像仍然良基。

函数体会被传给 WellFounded.fix,其中形参会被适当地打包与拆包,而递归调用则替换为对 WellFounded.fix 所提供值的调用。 由 Lean.Parser.Command.declaration : commanddecreasing_by 策略生成的终止性证明,会插入到恰当的位置。

最后,递归函数的等式定理与展开定理会从 WellFounded.fix_eq 推导出来。 这些定理隐藏了打包与拆包实参的细节,并以原始定义的形式描述函数行为。

在互递归的情况下,会用 PSum 把函数的实参合并起来,从而构造一个等价的非互递归函数,并在结果类型与函数体中对该和类型做模式匹配。

WellFounded 的定义建立在关系的可达元素这一概念之上:

🔗归纳谓词
WellFounded.{u} {α : Sort u} (r : α α Prop) : Prop
WellFounded.{u} {α : Sort u} (r : α α Prop) : Prop

如果 α 的所有元素在关系 r 下都是可及的,那么关系 rWellFounded 的。 若关系是 WellFounded 的,就不存在沿该关系的无限下降。

如果函数定义中递归调用的实参按照一个良基关系减小,那么该函数终止。 良基关系有时也称为 Artinian 关系,或称其满足“降链条件”。

WellFounded.intro.{u} {α : Sort u} {r : α  α  Prop}
  (h :  (a : α), Acc r a) : WellFounded r

若所有元素在 r 下都可及,则 r 良基。

🔗归纳谓词
Acc.{u} {α : Sort u} (r : α α Prop) : α Prop
Acc.{u} {α : Sort u} (r : α α Prop) : α Prop

Acc 是可及性谓词。给定关系 r(例如 <)和值 xAcc r x 表示 xr 下可及:

若不存在无限序列 ... < y₂ < y₁ < y₀ < x,则 x 可及。

Acc.intro.{u} {α : Sort u} {r : α  α  Prop} (x : α)
  (h :  (y : α), r y x  Acc r y) : Acc r x

如果对每个满足 r y xyy 也可及,那么 x 可及。注意,若不存在满足 r y xy,则 x 可及;这样的 x 称为一个基本情形

通过反复减法定义除法:终止性证明

通过反复减法定义除法的写法,也可以显式地借助良基递归来表达。

noncomputable def div (n k : Nat) : Nat := (inferInstance : WellFoundedRelation Nat).wf.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + (r (n - k) <| α:Type un✝:Natk:Natn:Natr:(y : Nat) WellFoundedRelation.rel y n Nath✝:¬k = 0h:¬k > nWellFoundedRelation.rel (n - k) n α:Type un✝:Natk:Natn:Natr:(y : Nat) WellFoundedRelation.rel y n Nath✝:¬k = 0h:¬k > nn - k < n All goals completed! 🐙)) n

该定义必须标记为 Lean.Parser.Command.declaration : commandnoncomputable,因为编译器不支持良基递归。 和 递归器 一样,它属于 Lean 逻辑的一部分。

这个除法定义应满足下列方程:

  • {n k : Nat}, (k = 0) div n k = 0

  • {n k : Nat}, (k > n) div n k = 0

  • {n k : Nat}, (k 0) (¬ k > n) div n k = 1 + div (n - k) k

这种归约行为并不 在定义上 成立:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 Tactic `rfl` failed: The left-hand side div n 0 is not definitionally equal to the right-hand side 0 n:Natdiv n 0 = 0n:Natdiv n 0 = 0
Tactic `rfl` failed: The left-hand side
  div n 0
is not definitionally equal to the right-hand side
  0

n:Natdiv n 0 = 0

不过,借助 WellFounded.fix_eq 展开良基递归之后,这三个方程都可以被证明成立:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 n:Nat_proof_2.fix (fun n r => if h : 0 = 0 then 0 else if h : 0 > n then 0 else 1 + r (n - 0) ) n = 0 All goals completed! 🐙 theorem div.eq1 : k > n div n k = 0 := k:Natn:Natk > n div n k = 0 k:Natn:Nath:k > ndiv n k = 0 k:Natn:Nath:k > n_proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) n = 0 k:Natn:Nath:k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) y) (n - k) ) = 0 k:Natn:Nath:k > n¬k = 0 k n 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ) (n - k) = 0 k:Natn:Nath:k > na✝¹:¬k = 0a✝:k n1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ) (n - k) = 0; All goals completed! 🐙 theorem div.eq2 : ¬ k = 0 ¬ (k > n) div n k = 1 + div (n - k) k := k:Natn:Nat¬k = 0 ¬k > n div n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > ndiv n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > n_proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) n = 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:¬k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) y) (n - k) ) = 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:k nn < k 0 = 1 + _proof_2.fix (fun n r => if h : n < k then 0 else 1 + r (n - k) ) (n - k) All goals completed! 🐙

7.6.4. 偏不动点递归🔗

所有定义在根本上都是方程:被定义的新常量等于定义的右侧。 对于以 结构递归 定义的函数,这个方程在 定义上成立,并且函数应用会返回唯一的值。 对于以 良基递归 定义的函数,这个方程可能只在 命题上成立,但函数对任意类型正确的实参应用,都等于定义所规定的相应值。 在这两种情形下,函数对所有输入都终止这一事实,意味着函数应用计算出的值总是唯一确定的。

在某些函数并非对所有实参都终止的情况下,这个方程未必能为每个输入唯一地确定返回值;但尽管如此,仍可能存在满足该定义方程的函数。 此时,仍可能把它定义为一个 偏不动点。 任何满足该定义方程的函数,都可以用来说明该方程不会造成逻辑矛盾,随后再把这个方程证明为该函数的定理。 和其他递归函数定义策略一样,编译后的代码会使用函数最初写下来的形式;类似于借助消去器或基于可达性证明的递归来定义函数,定义偏不动点所用到的函数,只是为了在 Lean 的逻辑中为其方程提供数学推理上的正当性。

术语 偏不动点 是 Lean 特有的。 凡是声明为 Lean.Parser.Command.declaration : commandpartial 的函数,只要其返回值类型可被占据,就不需要终止性证明;但从 Lean 逻辑的视角看,它们是完全不透明的。 而偏不动点则不同:在写证明时,可以按照其定义方程对它们进行重写。 从逻辑上说,偏不动点是一些全函数:把它们应用到实参上时不会 在定义上 归约,但 Lean 会为它们提供等式重写规则。 它们之所以称为“偏”,是因为定义方程未必会为所有可能的实参指定一个值。

偏不动点不仅能定义那些无法用结构递归或良基递归表达的函数;在其他情况下,这项技术同样有用。 即便某个定义方程已经完整描述了函数行为,且原则上也能用 良基递归 给出终止性证明,把函数定义为偏不动点仍可能更方便,因为这样无需书写终止性证明。

只有在显式请求时——即在定义上标注 Lean.Parser.Command.declaration : commandpartial_fixpoint——递归函数才会按偏不动点来定义。

可以定义为偏不动点的函数有两类:

  • 返回类型可被占据的尾递归函数

  • 返回值位于某个合适单子中的函数,例如 Option 单子

这两类函数都建立在同一套理论与构造之上:链完备偏序中单调方程的最小不动点。

与结构递归和良基递归一样,Lean 也允许把 互递归 函数定义为偏不动点。 要使用这一特性,互递归块 中的每个函数定义都必须带有 Lean.Parser.Command.declaration : commandpartial_fixpoint 修饰符。

按偏不动点定义

下面这个函数寻找使谓词 p 成立的最小自然数。 如果 p 永远不成立,那么这个方程并没有规定其行为:在这种情况下,函数 find 返回 42 或任意其他 Nat,都依然满足该方程。

def find (p : Nat Bool) (i : Nat := 0) : Nat := if p i then i else find p (i + 1) partial_fixpoint

精译器能够证明,满足该方程的函数确实存在。 在 Lean 的逻辑中,find 被定义为任意一个这样的函数。

7.6.4.1. 尾递归函数🔗

若满足下列两个条件,递归函数就可以定义为偏不动点:

  1. 函数的返回类型可被占据(与标记为 Lean.Parser.Command.declaration : commandpartial 的函数类似)——拥有 NonemptyInhabited 实例皆可。

  2. 所有递归调用都位于函数的 尾位置

若函数体中的一个表达式属于下列情形,则它处于 尾位置

  • 函数体本身;

  • 处于尾位置的 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式的各个分支;

  • 处于尾位置的 termIfThenElse : term`if c then t else e` 是 `ite c t e`(即“如果—那么—否则”)的记法;它根据 `c` 是否为真返回 `t` 或 `e`。 显式参数 `c : Prop` 本身没有计算内容;另有一个由实例合成得到的 `[Decidable c]` 参数,真正决定如何把 `c` 求值为真或假。 写成 `if h : c then t else e` 时表示依赖式条件 `dite`,此时 `t` 和 `e` 可以使用 `c` 为真或假的事实。 标识符中的记法约定:建议将 `if c then t else e` 写作 `ite`,并分别用 `left`、`right` 指代 `t`、`e`。if 表达式的各个分支;

  • 处于尾位置的 Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` The *anaphoric let* `let := v` defines a variable called `this`. let 表达式的函数体。

特别地,Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式的 判别项termIfThenElse : term`if c then t else e` 是 `ite c t e`(即“如果—那么—否则”)的记法;它根据 `c` 是否为真返回 `t` 或 `e`。 显式参数 `c : Prop` 本身没有计算内容;另有一个由实例合成得到的 `[Decidable c]` 参数,真正决定如何把 `c` 求值为真或假。 写成 `if h : c then t else e` 时表示依赖式条件 `dite`,此时 `t` 和 `e` 可以使用 `c` 为真或假的事实。 标识符中的记法约定:建议将 `if c then t else e` 写作 `ite`,并分别用 `left`、`right` 指代 `t`、`e`。if 表达式的条件,以及函数实参,都不处于尾位置。

循环也是尾递归函数

由于函数体本身就是一个 尾位置,无限循环函数 loop 是尾递归的。 它可以定义为偏不动点。

def loop (x : Nat) : Nat := loop (x + 1) partial_fixpoint
带分支的尾递归

Array.find 也可以借助良基递归加上终止性证明来构造,但用 Lean.Parser.Command.declaration : commandpartial_fixpoint 来定义往往更方便,因为这样不需要终止性证明。

def Array.find (xs : Array α) (p : α Bool) (i : Nat := 0) : Option α := if h : i < xs.size then if p xs[i] then some xs[i] else Array.find xs p (i + 1) else none partial_fixpoint

如果递归调用的结果不是直接返回,而是先传给另一个函数,那么它就不在尾位置,此定义也就会失败。

def List.findIndex (xs : List α) (p : α Bool) : Int := match xs with | [] => -1 | x::ys => if p x then 0 else have r := Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in if ys✝.findIndex p = -1 then -1 else ys✝.findIndex p + 1 Tried to apply 'monotone_ite', but failed. Possible cause: A missing `MonoBind` instance. Use `set_option trace.Elab.Tactic.monotonicity true` to debug.List.findIndex ys p if r = -1 then -1 else r + 1 partial_fixpoint

递归调用处的错误消息是:

Could not prove 'List.findIndex' to be monotone in its recursive calls:
  Cannot eliminate recursive call `List.findIndex ys p` enclosed in
    if ys✝.findIndex p = -1 then -1 else ys✝.findIndex p + 1
  Tried to apply 'monotone_ite', but failed.
  Possible cause: A missing `MonoBind` instance.
  Use `set_option trace.Elab.Tactic.monotonicity true` to debug.

7.6.4.2. 单子函数🔗

如果函数的返回类型是某个带有 Lean.Order.MonoBind 实例的单子(例如 Option),那么把函数定义为偏不动点会更强大。 这时,递归调用不再局限于尾位置,还可以出现在 bindList.mapM 等高阶单子函数内部。

能够支持这一点的高阶函数集合是可扩展的,因此这里不给出穷尽列表。 理想状态是:只要一个单子递归函数定义是通过 bind 这类抽象单子操作构造出来的,并且没有拆开单子的抽象(例如对 Option 的值做模式匹配),它就应该被接受。 特别地,使用 Lean.Parser.Term.do : termdo 记法 应当可行。

单子函数

下面这个函数在 Option 单子中实现了 Ackermann 函数,并且无需显式或隐式终止性证明即可被接受:

def ack : (n m : Nat) Option Nat | 0, y => some (y+1) | x+1, 0 => ack x 1 | x+1, y+1 => do ack x ( ack (x+1) y) partial_fixpoint

如果适当设置,递归调用也可以出现在 List.mapM 之类的高阶函数内部,以及 Lean.Parser.Term.do : termdo 记法 中:

structure Tree where cs : List Tree def Tree.rev (t : Tree) : Option Tree := do Tree.mk ( t.cs.reverse.mapM (Tree.rev ·)) partial_fixpoint def Tree.rev' (t : Tree) : Option Tree := do let mut cs := [] for c in t.cs do cs := ( c.rev') :: cs return Tree.mk cs partial_fixpoint

若对递归调用的结果做模式匹配,就会阻止该定义作为偏不动点通过:

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else match Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in match ys✝.findIndex p with | none => none | some r => some (r + 1) List.findIndex ys p with | none => none | some r => some (r + 1) partial_fixpoint
Could not prove 'List.findIndex' to be monotone in its recursive calls:
  Cannot eliminate recursive call `List.findIndex ys p` enclosed in
    match ys✝.findIndex p with
    | none => none
    | some r => some (r + 1)
  

在这个具体例子里,用 Functor.map 代替显式模式匹配就有帮助:

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else (· + 1) <$> List.findIndex ys p partial_fixpoint

7.6.4.3. 偏正确性定理🔗

对于每个定义为偏不动点的函数,Lean 都会证明其定义方程成立。 这使得人们可以通过重写来进行证明。 不过,这些等式定理不足以推理函数在那些其规范本身不终止的实参上的行为。 在运行时会导致无限递归的代码路径,在证明中最终只会变成无限长的重写链。

另一方面,在合适单子中的偏不动点还会提供额外定理,把“不终止”所对应的未定义值映射为该单子中的适当值。 在 Option 单子中,当定义方程规定某些输入上不终止时,偏不动点在这些输入上的值就等于 Option.none。 基于这一事实,Lean 会为该函数证明一个 偏正确性定理,使人们能够在函数结果为 Option.some 时推出相应事实。

偏正确性定理

回忆前面的例子 List.findIndex

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else (· + 1) <$> List.findIndex ys p partial_fixpoint

有了这个函数定义,Lean 会自动证明下面的偏正确性定理:

List.findIndex.partial_correctness.{u_1} {α : Type u_1} (p : α Bool) (motive : List α Nat Prop) (h : (findIndex : List α Option Nat), ( (xs : List α) (r : Nat), findIndex xs = some r motive xs r) (xs : List α) (r : Nat), (match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r motive xs r) (xs : List α) (r : Nat) : xs.findIndex p = some r motive xs r

这里的动机(motive)是 List.findIndex 的参数类型与返回类型之间的一个关系,其中返回类型里的 Option 已被去掉。 若给定一个签名与 List.findIndex 相容的任意偏函数,并且满足下列条件:

  • 对所有该任意函数返回某个值(而不是 none)的输入,动机都成立;

  • 按定义方程进行一步重写、并把其中递归调用替换为该任意函数后,也能推出动机成立;

那么,对所有 List.findIndex 返回 some 的输入,动机都成立。

偏正确性定理是一条推理原理。 它可以用来证明:得到的数字是该列表中的一个合法索引,而且谓词在该索引处成立:

theorem List.findIndex_implies_pred (xs : List α) (p : α Bool) : xs.findIndex p = some i x, xs[i]? = some x p x := α:Type u_1i:Natxs:List αp:α Boolxs.findIndex p = some i x, xs[i]? = some x p x = true α:Type u_1i:Natxs:List αp:α Bool (findIndex : List α Option Nat), (∀ (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = true) (xs : List α) (r : Nat), (match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r x, xs[r]? = some x p x = true α:Type u_1i:Natxs✝:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truexs:List αr:Nathsome:(match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r x, xs[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αhsome:none = some r x, [][r]? = some x p x = trueα:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx✝:αys✝:List αhsome:(if p x✝ = true then some 0 else (fun x => x + 1) <$> findIndex ys✝) = some r x, (x✝ :: ys✝)[r]? = some x p x = true next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αhsome:none = some r x, [][r]? = some x p x = true All goals completed! 🐙 next x ys α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αhsome:(if p x✝ = true then some 0 else (fun x => x + 1) <$> findIndex ys✝) = some r x, (x✝ :: ys✝)[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r x, (x✝ :: ys✝)[r]? = some x p x = trueα:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r x, (x✝ :: ys✝)[r]? = some x p x = true next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r x, (x✝ :: ys✝)[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some rthis:r = 0 x_1, (x :: ys)[r]? = some x_1 p x_1 = true All goals completed! 🐙 next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r x, (x✝ :: ys✝)[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome: a, findIndex ys = some a a + 1 = r x, (x✝ :: ys✝)[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truexs✝:List αx:αys:List αh✝:¬p x = truer':Nathr:findIndex ys = some r' x_1, (x :: ys)[r' + 1]? = some x_1 p x_1 = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natxs✝:List αx:αys:List αh✝:¬p x = truer':Natih: x, ys[r']? = some x p x = truehr:findIndex ys = some r' x_1, (x :: ys)[r' + 1]? = some x_1 p x_1 = true All goals completed! 🐙

7.6.4.4. 偏不动点下的互递归🔗

Lean 支持使用 偏不动点 来定义 互递归 函数。 互递归既可以通过 互递归块 引入,也可能来自 Lean.Parser.Term.letrec : termlet rec 表达式和 Lean.Parser.Command.declaration : commandwhere 代码块。 带偏不动点的互递归规则,会应用到由互递归组的精译步骤所得、经过提升后且实际上互相递归的一组定义上。

若互递归组中的所有函数都带有 Lean.Parser.Command.declaration : commandpartial_fixpoint 子句,就会采用这一策略。

7.6.4.5. 理论与构造🔗

该构造建立在 Knaster–Tarski 定理的一个变体之上:在链完备偏序中,每个单调函数都有最小不动点。

所需理论位于 Lean.Order 命名空间中。 它并非旨在成为通用的序理论结果库。 相反,Lean.Order 中的定义和定理仅用作 Lean.Parser.Command.declaration : commandpartial_fixpoint 功能的实现细节,应将其视为可能随时变更而不另行通知的私有 API。

偏序和链完备偏序的概念分别由类型类 Lean.Order.PartialOrderLean.Order.CCPO 表示。

🔗类型类
Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)
Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)

偏序是一个自反、传递且反对称的关系。

此类型类用于构造 partial_fixpoint,不应作其他用途。

Lean.Order.PartialOrder.mk.{u}
rel : α  α  Prop

“小于等于”关系,亦可理解为“近似”关系。

此关系用于构造 partial_fixpoint,不应作其他用途。

rel_refl :  {x : α}, x  x

“小于等于”关系(或“近似”关系)是自反的。

rel_trans :  {x y z : α}, x  y  y  z  x  z

“小于等于”关系(或“近似”关系)是传递的。

rel_antisymm :  {x y : α}, x  y  y  x  x = y

“小于等于”关系(或“近似”关系)是反对称的。

🔗类型类
Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)
Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)

链完备偏序(CCPO)是一种偏序,其中每条链都有最小上界。

此类型类用于构造 partial_fixpoint,不应作其他用途。

Lean.Order.CCPO.mk.{u}
rel : α  α  Prop

继承自父结构。

rel_refl :  {x : α}, x  x

继承自父结构。

rel_trans :  {x y z : α}, x  y  y  z  x  z

继承自父结构。

rel_antisymm :  {x y : α}, x  y  y  x  x = y

继承自父结构。

has_csup :  {c : α  Prop}, chain c  Exists (is_sup c)

每条链的最小上界都存在。

如果函数保持偏序关系,它就是单调的。 也就是说,若 x y,则 f x f y。 运算符 表示 Lean.Order.PartialOrder.rel

🔗定义
Lean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α β) : Prop
Lean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α β) : Prop

若函数把相关元素映射为相关元素,则该函数是单调的。

此定义用于构造 partial_fixpoint,不应作其他用途。

可使用 fix 取得单调函数的不动点;如 fix_eq 所示,它确实构造了一个不动点。

🔗定义
Lean.Order.fix.{u} {α : Sort u} [CCPO α] (f : α α) (hmono : monotone f) : α
Lean.Order.fix.{u} {α : Sort u} [CCPO α] (f : α α) (hmono : monotone f) : α

单调函数的最小不动点,是对该函数进行超限迭代所得链的最小上界。

定义本身并非严格需要 monotone f 假设;然而没有该假设时,定义并没有太大意义。 此外,让每次使用 fix 时都带上单调性要求,也可简化 fix_eq 等定理的应用。

此定义用于构造 partial_fixpoint,不应作其他用途。

🔗定理
Lean.Order.fix_eq.{u} {α : Sort u} [CCPO α] {f : α α} (hf : monotone f) : fix f hf = f (fix f hf)
Lean.Order.fix_eq.{u} {α : Sort u} [CCPO α] {f : α α} (hf : monotone f) : fix f hf = f (fix f hf)

链完备偏序中单调函数的不动点主定理:fix 构造出的值确实是不动点。

此定理用于构造 partial_fixpoint,不应作其他用途。

为了构造偏不动点,Lean 首先合成合适的 CCPO 实例。

  • 如果函数的结果类型有专用实例,例如 OptioninstCCPOOption,就将其与函数类型的实例 instCCPOPi 一起使用,为整个函数类型构造实例。

  • 否则,如果可以证明函数类型由见证 w 居留,则使用包装类型 FlatOrder w 的实例 FlatOrder.instCCPO。在此序中,w 是最小元素,所有其他元素彼此不可比。

接下来,将函数定义右侧的递归调用抽象出来;它们会成为 fix 的参数 f。单调性要求由 monotonicity 策略解决,该策略以语法驱动的方式应用组合式单调性引理。

该策略通过以下步骤解决形如 monotone (fun x => x ) 的目标:

  • 当不再依赖 x 时,应用 monotone_const

  • Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 表达式分情况。

  • termIfThenElse : term`if c then t else e` 是 `ite c t e`(即“如果—那么—否则”)的记法;它根据 `c` 是否为真返回 `t` 或 `e`。 显式参数 `c : Prop` 本身没有计算内容;另有一个由实例合成得到的 `[Decidable c]` 参数,真正决定如何把 `c` 求值为真或假。 写成 `if h : c then t else e` 时表示依赖式条件 `dite`,此时 `t` 和 `e` 可以使用 `c` 为真或假的事实。 标识符中的记法约定:建议将 `if c then t else e` 写作 `ite`,并分别用 `left`、`right` 指代 `t`、`e`。if 表达式分情况。

  • 如果值和类型均不依赖 x,则将 Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` The *anaphoric let* `let := v` defines a variable called `this`. let 表达式移入上下文。

  • 当值和类型确实依赖 x 时,对 Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` The *anaphoric let* `let := v` defines a variable called `this`. let 表达式进行 zeta 归约。

  • 应用以 partial_fixpoint_monotone 标注的引理

系统注册了以下单调性引理;它们应当允许递归调用出现在给定高阶函数中以 · 标示的参数位置(但不允许出现在以 _ 标示的其他参数位置)。

定理

模式

monotone_allM

Array.allM · _ _ _

monotone_anyM

Array.anyM · _ _ _

monotone_anyM_loop

Array.anyM.loop · _ _ _

monotone_array_filterMapM

Array.filterMapM · _

monotone_array_forM

Array.forM · _ _ _

monotone_array_forRevM

Array.forRevM · _ _ _

monotone_findIdxM?

Array.findIdxM? · _

monotone_findM?

Array.findM? · _

monotone_findRevM?

Array.findRevM? · _

monotone_findSomeM?

Array.findSomeM? · _

monotone_findSomeRevM?

Array.findSomeRevM? · _

monotone_flatMapM

Array.flatMapM · _

monotone_foldlM

Array.foldlM · _ _ _ _

monotone_foldlM_loop

Array.foldlM.loop · _ _ _ _ _

monotone_foldrM

Array.foldrM · _ _ _ _

monotone_foldrM_fold

Array.foldrM.fold · _ _ _ _

monotone_forIn

forIn _ _ ·

monotone_forIn'

forIn' _ _ ·

monotone_forIn'_loop

Array.forIn'.loop _ · _ _

monotone_mapFinIdxM

_.mapFinIdxM ·

monotone_mapM

Array.mapM · _

monotone_modifyM

_.modifyM _ ·

monotone_map

_ <$> ·

monotone_allM

List.allM · _

monotone_anyM

List.anyM · _

monotone_filterAuxM

List.filterAuxM · _ _

monotone_filterM

List.filterM · _

monotone_filterRevM

List.filterRevM · _

monotone_findM?

List.findM? · _

monotone_findSomeM?

List.findSomeM? · _

monotone_foldlM

List.foldlM · _ _

monotone_foldrM

List.foldrM · _ _

monotone_forIn

forIn _ _ ·

monotone_forIn'

forIn' _ _ ·

monotone_forIn'_loop

List.forIn'.loop _ · _ _

monotone_forM

_.forM ·

monotone_mapM

List.mapM · _

monotone_bindM

Option.bindM · _

monotone_elimM

Option.elimM · · ·

monotone_getDM

_.getDM ·

monotone_mapM

Option.mapM · _

monotone_fst

·.fst

monotone_mk

·, ·

monotone_snd

·.snd

monotone_seq

· <*> ·

monotone_seqLeft

· <* ·

monotone_seqRight

· *> ·

coind_impl

· ·

coind_monotone_and

· ·

coind_monotone_exists

Exists ·

coind_monotone_forall

(y : _), _ _ _

coind_monotone_or

· ·

coind_not

¬·

implication_order_monotone_and

· ·

implication_order_monotone_exists

Exists ·

implication_order_monotone_forall

(y : _), _ _ _

implication_order_monotone_or

· ·

ind_impl

· ·

ind_not

¬·

monotone_bind

· >>= ·

monotone_dite

dite _ · ·

monotone_exceptTRun

·.run

monotone_ite

if _ then · else ·

monotone_optionTRun

·.run

monotone_readerTRun

·.run _

monotone_stateRefT'Run

·.run _

monotone_stateTRun

·.run _

这里描述的序理论框架也是余归纳与归纳谓词的基础。 对于取值于 Prop 的函数,Lean.Order.CompleteLattice 实例同时提供最小与最大不动点,从而允许使用 Lean.Parser.Command.declaration : commandinductive_fixpointLean.Parser.Command.declaration : commandcoinductive_fixpoint 子句进行定义。

7.6.5. 余归纳与归纳谓词🔗

Lean 的类型论并不直接支持余归纳类型。 不过,余归纳谓词——也就是取值于 Prop 的递归定义——可以借助命题上的完备格结构来定义。 这些谓词提供了一种余归纳推理原理:若能证明某个对象满足某个更小的谓词,且该谓词本身与余归纳谓词的定义相容,就可以证明该对象满足这个余归纳谓词。 这与归纳推理对偶:在归纳推理中,一个已知事实可以通过可能递归的分类讨论被分解。 余归纳谓词使得人们能够刻画并推理无限域。 计算机科学中的一些例子包括:

  • 允许环路的状态迁移系统上的互模拟

  • 小步操作语义中的发散

  • 活性性质

对偶地,归纳谓词 也可以借助同样的机制,通过最小不动点来定义。 由于它们使用的是同一套底层机制,这种替代普通 归纳类型 的方案,与归纳—余归纳混合的互递归块相兼容。

无限序列

给定 α 上的一个关系 R(即其类型为 α α Prop),如果满足下列条件,就存在一个从 x 出发的、由 α 中值组成的无限序列:

  • 存在某个 y 使得 R x y 成立;

  • 并且从 y 出发也存在一个无限序列。

这是一个典型的余归纳谓词:它描述的是一种潜在无限的行为,并且可以表达为一条没有基例的单一推理规则。

这个递归规格是良定义的,但它不能作为普通递归函数来定义,因为定义中的递归部分并没有减小。 不过,把它定义成余归纳定义却完全合理:

coinductive InfSeq (R : α α Prop) : α Prop where | step (y : α) : R x y InfSeq R y InfSeq R x

余归纳推理原理接受一个谓词 pred。 要证明 a 是某条无限 R-序列的起点,只需证明:对每个满足 pred 的元素,R 都会把它关联到另一个同样满足该谓词的元素。 换言之,无限序列的存在可以通过直接给出这样一条序列来证明:

InfSeq.coinduct (R : α α Prop) (pred : α Prop) : ( (a : α), pred a y, R a y pred y) (a : α), pred a InfSeq R a

在 Lean 中,有两种方式定义余归纳谓词:

  1. 在取值于 Prop 的递归 Lean.Parser.Command.declaration : commanddef 上使用 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 终止性子句,它会取最大不动点。等价地,Lean.Parser.Command.declaration : commandinductive_fixpoint 子句则把归纳谓词定义为最小不动点。

  2. 使用 Lean.Parser.Command.coinductivecoinductive 命令,它提供了一种与 Lean.Parser.Command.inductiveIn Lean, every concrete type other than the universes and every type constructor other than dependent arrows is an instance of a general family of type constructions known as inductive types. It is remarkable that it is possible to construct a substantial edifice of mathematics based on nothing more than the type universes, dependent arrow types, and inductive types; everything else follows from those. Intuitively, an inductive type is built up from a specified list of constructors. For example, `List α` is the list of elements of type `α`, and is defined as follows: ``` inductive List (α : Type u) where | nil | cons (head : α) (tail : List α) ``` A list of elements of type `α` is either the empty list, `nil`, or an element `head : α` followed by a list `tail : List α`. See [Inductive types](https://lean-lang.org/theorem_proving_in_lean4/inductive_types.html) for more information. inductive 声明相呼应的声明式语法。

7.6.5.1. 不动点终止性子句🔗

取值于 Prop 的递归函数,可以通过为其添加 Lean.Parser.Command.declaration : commandcoinductive_fixpoint(用于余归纳定义,即最大不动点)或 Lean.Parser.Command.declaration : commandinductive_fixpoint(用于归纳定义,即最小不动点)标注,来定义为一个不动点。 这些终止性子句与 Lean.Parser.Command.declaration : commandpartial_fixpoint 扮演相同角色,但它们利用 Prop 上的完备格结构 来计算相应的不动点。

7.6.5.1.1. 余归纳不动点🔗

Lean.Parser.Command.declaration : commandcoinductive_fixpoint 子句把一个谓词定义为其定义方程的最大不动点。 该函数必须相对于 Lean.Order.ReverseImplicationOrder 是单调的;在这个顺序中,P Q 表示 Q P

这个顺序会按点扩展到谓词的定义域上。 给定 α 上的谓词 PQP Q 表示 x : α, P x Q x(也就是 x, Q x P x)。

无限序列的单调性

当存在一条从 a 出发、由 R 关联起来的无限链时,命题 InfSeq R a 为真。 这可以用 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 写成:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

在精译过程中,第一步是把这个递归定义对递归调用做抽象,得到一个与 F 等价的定义:

def F (R : α α Prop) (a : α) (P : α Prop) : Prop := b, R a b P b

要使这个函数相对于反向蕴含顺序是单调的,它就必须保持 PQ 之间的反向蕴含顺序。 也就是说, (x : α), Q x P x 必须推出 (x : α), F R x Q F R x P

theorem F_monotone (h : (x : α), Q x P x) : (x : α), F R x Q F R x P := α:Sort u_1R:α α PropQ:α PropP:α Proph: (x : α), Q x P x (x : α), F R x Q F R x P All goals completed! 🐙
单调性失败

如果某个元素不存在一条通向它的无限链,那么它对于该关系就是可达的。 标准库中将这一性质归纳地定义为 Acc。 下面这个把它尝试定义为余归纳谓词的做法会失败:

Could not prove 'NoInfChain' to be monotone in its recursive calls: Cannot eliminate recursive call in NoInfChain R y✝ def NoInfChain (R : α α Prop) (x : α) : Prop := y, R x y ¬NoInfChain R y coinductive_fixpoint
Could not prove 'NoInfChain' to be monotone in its recursive calls:
  Cannot eliminate recursive call in
    NoInfChain R y✝
  

对应的函数是:

def F (R : α α Prop) (x : α) (P : α Prop) : Prop := y, R x y ¬P y

Lean 之所以无法证明这个函数单调,是因为它事实上确实不单调:

theorem F_nonmonotone : ¬( α R P Q, ( (x : α), Q x P x) ( (x : α), F R x Q F R x P)) := ¬ (α : Sort u_1) (R : α α Prop) (P Q : α Prop), (∀ (x : α), Q x P x) (x : α), F R x Q F R x P α R P Q, ¬((∀ (x : α), Q x P x) (x : α), F R x Q F R x P) -- α = PUnit, R always true P Q, ¬((∀ (x : PUnit), Q x P x) (x : PUnit), F (fun x x_1 => True) x Q F (fun x x_1 => True) x P) -- P 恒为真,而 Q 恒为假 ¬((∀ (x : PUnit), (fun x => False) x (fun x => True) x) (x : PUnit), (F (fun x x_1 => True) x fun x => False) F (fun x x_1 => True) x fun x => True) All goals completed! 🐙
非谓词

某个命题的无限合取可以定义为一个余归纳不动点:

def InfConj (p : Prop) : Prop := p InfConj p coinductive_fixpoint

不过,这不能用来定义一个无限积:

def InfProd (α : Type) : Prop := α × Application type mismatch: The argument InfProd α has type Prop of sort `Type` but is expected to have type Type ?u.3 of sort `Type (?u.3 + 1)` in the application α × InfProd αInfProd α unused `coinductive_fixpoint`, function is not recursivecoinductive_fixpoint

错误消息表明,此处本来期望的是一个命题:

Application type mismatch: The argument
  InfProd α
has type
  Prop
of sort `Type` but is expected to have type
  Type ?u.3
of sort `Type (?u.3 + 1)` in the application
  α × InfProd α

与通过偏不动点给出的定义一样,余归纳谓词的定义方程并不在定义上成立。 不过,精译器会证明等式引理,从而允许把该谓词重写为其展开式。

定义相等与余归纳谓词

InfSeq 是一个余归纳断言:某个关系从某点开始存在一条无限链:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

由于它是借助 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 定义的,因此它与其展开式并不在定义上相等:

example (R : α α Prop) (a : α) : InfSeq R a = b, R a b InfSeq R b := α:Sort u_1R:α α Propa:αInfSeq R a = b, R a b InfSeq R b Tactic `rfl` failed: The left-hand side InfSeq R a is not definitionally equal to the right-hand side b, R a b InfSeq R b α:Sort u_1R:α α Propa:αInfSeq R a = b, R a b InfSeq R bα:Sort u_1R:α α Propa:αInfSeq R a = b, R a b InfSeq R b
Tactic `rfl` failed: The left-hand side
  InfSeq R a
is not definitionally equal to the right-hand side
   b, R a b  InfSeq R b

α:Sort u_1R:α  α  Propa:αInfSeq R a =  b, R a b  InfSeq R b

不过,它带有可将其重写为展开式的等式引理:

example (R : α α Prop) (a : α) : InfSeq R a = b, R a b InfSeq R b := α:Sort u_1R:α α Propa:αInfSeq R a = b, R a b InfSeq R b All goals completed! 🐙

除了等式引理外,Lean 还会生成一条 余归纳原理。 这条余归纳原理说明:只要给出另一个谓词,并证明它是该单调函数的一个后不动点,就可以证明相应的余归纳谓词。

无限序列的余归纳原理

InfSeq 是一个余归纳断言:某个关系从某点开始存在一条无限链:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

对应的单调函数是:

def F (R : α α Prop) (a : α) (P : α Prop) : Prop := b, R a b P b

由于 InfSeqF最大不动点,只要存在任意一个谓词,它小于自己在 F 下的像,就足以说明:凡满足该谓词的元素,也都满足 InfSeq。 换言之,要证明 InfSeq R a,只需给出一个谓词 P,使得 (a : α), P a F R a P,也就是 (a : α), P a b, R a b P b,然后再证明 P a

这条余归纳原理名为 InfSeq.coinduct

InfSeq.coinduct {α} (R : α α Prop) (pred : α Prop) : ( (a : α), pred a b, R a b pred b) (a : α), pred a InfSeq R a
余归纳的简单证明

InfSeq 断言:在给定起点处,某个关系中存在一条由元素组成的无限序列:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

如果 R a a 成立,那么就存在一条在 a 处自环的平凡无限链:

theorem cycle_InfSeq {R : α α Prop} (a : α) : R a a InfSeq R a := α:Sort u_1R:α α Propa:αR a a InfSeq R a α:Sort u_1R:α α Propa:α (a : α), R a a b, R a b R b b α:Sort u_1R:α α Propa:αx:αh:R x x b, R x b R b b All goals completed! 🐙
小于关系的无限链

InfSeq 断言:在给定起点处,某个关系中存在一条由元素组成的无限序列:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

对于关系 (· < ·),自然数上存在无限链。 每个自然数都可以作为这样一条链的起点,因此这里的谓词可以取成平凡谓词:

theorem lt_InfSeq {n : Nat} : InfSeq (· < ·) n := n:NatInfSeq (fun x1 x2 => x1 < x2) n n:Nat (a : Nat), True b, a < b Truen:NatTrue n:Nat (a : Nat), True b, a < b True n:Natk:Natx✝:True b, k < b True n:Natk:Natx✝:Truek < k + 1 True All goals completed! 🐙 n:NatTrue All goals completed! 🐙
DFA 语言等价性

余归纳谓词天然适合刻画类似互模拟的概念。

一个确定有限自动机由如下数据给出:状态集合 Q、字母表 A、位于 Q 中的初始状态 q、用来定义接受状态的 Q 的一个子集,以及一个把状态和字母表元素映射到新状态的迁移函数:

structure DFA (Q : Type) (A : Type) : Type where q₀ : Q δ : Q A Q accepting : Q Bool

对于同一字母表上的两个自动机,如果从给定的一对状态出发,它们对“这些状态是否为接受状态”的判断一致,并且按照各自的迁移函数,从所有后继状态出发得到的语言也都等价,那么它们在这对状态上的语言就是等价的:

def languageEquivalent (M : DFA Q A) (M' : DFA Q' A) (q : Q) (q' : Q') : Prop := M.accepting q = M'.accepting q' (a : A), languageEquivalent M M' (M.δ q a) (M'.δ q' a) coinductive_fixpoint

余归纳原理刻画了确定自动机的标准互模拟概念:

languageEquivalent.coinduct {Q A Q' : Type} (M : DFA Q A) (M' : DFA Q' A) (pred : Q Q' Prop) : ( (q : Q) (q' : Q'), pred q q' M.accepting q = M'.accepting q' (a : A), pred (M.δ q a) (M'.δ q' a)) (q : Q) (q' : Q'), pred q q' languageEquivalent M M' q q'

它可以用来证明下面这两个 DFA 的语言等价:

b a, b a fail ok
a, b b b a a fail ok start

这两个 DFA 可以用如下定义表示:

inductive Alphabet where | a | b inductive Q1 where | ok | fail def loop : DFA Q1 Alphabet where q₀ := .ok δ | .ok, .a => .ok | _, _ => .fail accepting | .ok => True | _ => False inductive Q2 where | start | ok | fail def cycle : DFA Q2 Alphabet where q₀ := .start δ | .start, .a => .ok | .ok, .a => .start | _, _ => .fail accepting | .start | .ok => True | .fail => False

为了证明它们等价,第一步是定义一个关系,用来刻画它们的等价状态。 然后,余归纳会把“它们在该关系下确实等价”的证明提升为语言等价:

theorem loop_equiv_cycle : languageEquivalent loop cycle loop.q₀ cycle.q₀ := languageEquivalent loop cycle loop.q₀ cycle.q₀ r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => FalselanguageEquivalent loop cycle loop.q₀ cycle.q₀ r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => False (q : Q1) (q' : Q2), r q q' loop.accepting q = cycle.accepting q' (a : Alphabet), r (loop.δ q a) (cycle.δ q' a)r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => Falser loop.q₀ cycle.q₀ r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => False (q : Q1) (q' : Q2), r q q' loop.accepting q = cycle.accepting q' (a : Alphabet), r (loop.δ q a) (cycle.δ q' a) r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => False (q : Q1) (q' : Q2), r q q' ((match q with | Q1.ok => decide True | x => decide False) = match q' with | Q2.start => decide True | Q2.ok => decide True | Q2.fail => decide False) (a : Alphabet), r (match q, a with | Q1.ok, Alphabet.a => Q1.ok | x, x_1 => Q1.fail) (match q', a with | Q2.start, Alphabet.a => Q2.ok | Q2.ok, Alphabet.a => Q2.start | x, x_1 => Q2.fail) r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => False (q : Q1) (q' : Q2), r q q' ((match q with | Q1.ok => decide True | x => decide False) = match q' with | Q2.start => decide True | Q2.ok => decide True | Q2.fail => decide False) (a : Alphabet), r (match q, a with | Q1.ok, Alphabet.a => Q1.ok | x, x_1 => Q1.fail) (match q', a with | Q2.start, Alphabet.a => Q2.ok | Q2.ok, Alphabet.a => Q2.start | x, x_1 => Q2.fail) All goals completed! 🐙 r:Q1 Q2 Prop := fun x x_1 => match x, x_1 with | Q1.ok, Q2.start => True | Q1.ok, Q2.ok => True | Q1.fail, Q2.fail => True | x, x_2 => Falser loop.q₀ cycle.q₀ All goals completed! 🐙

7.6.5.1.2. 归纳不动点🔗

Lean.Parser.Command.declaration : commandinductive_fixpoint 子句把一个谓词定义为其定义方程的最小不动点。 该函数必须相对于 Lean.Order.ImplicationOrder 是单调的;这是 Prop 上的一个顺序,其中 P ⊑ Q 表示 P → Q。 这为谓词提供了普通 Lean.Parser.Command.declaration : commandinductive 类型声明之外的另一种选择,并且与 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 对偶。

在大多数情况下,普通的归纳类型声明会更方便。 不过,归纳不动点定义相较于普通归纳类型声明有两个关键优势,因此更适合某些专门用途:

  • 普通归纳类型声明带有一个句法性的正性条件:归纳类型的递归出现不能位于负位置。而归纳不动点要求的则是单调性,这是一条语义性的条件。

  • 归纳不动点可以与余归纳不动点互相定义,从而允许归纳—余归纳混合谓词。

对于每个归纳不动点定义,系统都会自动证明一条归纳原理。 这条归纳原理在逻辑强度上与归纳类型声明会生成的对应归纳原理相同,但其表述方式略有不同,而且必须显式应用。

与余归纳不动点一样,归纳不动点定义也不会在定义上归约。 它们可以借助自动生成的等式引理来展开,而其归纳原理则允许在证明中使用它们。

作为归纳不动点的自反传递闭包

一个关系的自反传递闭包可以定义为归纳谓词:

inductive Star (R : α α Prop) : α α Prop where | refl : x : α, Star R x x | step : x y z, R x y Star R y z Star R x z

同一个谓词也可以定义为最小不动点。

def StarInd (tr : α α Prop) (q₁ q₂ : α) : Prop := q₁ = q₂ (z : α), (tr q₁ z StarInd tr z q₂) inductive_fixpoint

系统会生成一条归纳原理:

StarInd.induct (tr : α α Prop) (q₂ : α) (pred : α Prop) (hyp : (q₁ : α), (q₁ = q₂ z, tr q₁ z pred z) pred q₁) (q₁ : α) : StarInd tr q₁ q₂ pred q₁

这条归纳原理可以用来证明这两种表述彼此等价:

theorem star_implies_starInd (R : α α Prop) : a b : α, Star R a b = StarInd R a b := α:Sort u_1R:α α Prop (a b : α), Star R a b = StarInd R a b α:Sort u_1R:α α Propa:αb:αStar R a b = StarInd R a b α:Sort u_1R:α α Propa:αb:αStar R a b StarInd R a b α:Sort u_1R:α α Propa:αb:αStar R a b StarInd R a bα:Sort u_1R:α α Propa:αb:αStarInd R a b Star R a b α:Sort u_1R:α α Propa:αb:αStar R a b StarInd R a b α:Sort u_1R:α α Propa:αb:αh:Star R a bStarInd R a b α:Sort u_1R:α α Propa:αb:αx✝:αStarInd R x✝ x✝α:Sort u_1R:α α Propa:αb:αx✝:αy✝:αz✝:αa✝¹:R x✝ y✝a✝:Star R y✝ z✝a_ih✝:StarInd R y✝ z✝StarInd R x✝ z✝ α:Sort u_1R:α α Propa:αb:αx✝:αStarInd R x✝ x✝α:Sort u_1R:α α Propa:αb:αx✝:αy✝:αz✝:αa✝¹:R x✝ y✝a✝:Star R y✝ z✝a_ih✝:StarInd R y✝ z✝StarInd R x✝ z✝ All goals completed! 🐙 α:Sort u_1R:α α Propa:αb:αStarInd R a b Star R a b α:Sort u_1R:α α Propa:αb:α (q₁ : α), (q₁ = b z, R q₁ z Star R z b) Star R q₁ b All goals completed! 🐙

7.6.5.1.3. 互递归块中的归纳-余归纳混合谓词🔗

互递归块 可以混用 Lean.Parser.Command.declaration : commandcoinductive_fixpointLean.Parser.Command.declaration : commandinductive_fixpoint 子句。 块中的每个定义都必须使用这两种子句之一。 该构造会使用 Prop 上的两种格结构:归纳定义使用 ImplicationOrder,余归纳定义使用 ReverseImplicationOrder。 在这两种情况下,系统计算的都是相应格上的最小不动点;而在反向蕴含顺序下,这个最小不动点恰好对应标准顺序下的最大不动点。 之所以可行,是因为遇到否定或蕴含时,单调性 引理会在这两种顺序之间翻转方向。

归纳-余归纳混合互递归块

这个互递归块包含互相递归的余归纳谓词与归纳谓词:

mutual def tick : Prop := ¬tock coinductive_fixpoint def tock : Prop := ¬tick inductive_fixpoint end

系统会为互递归块中的第一个定义生成一条互归纳原理:

tick.mutual_induct (pred_1 pred_2 : Prop) : (pred_1 pred_2 False) ((pred_1 False) pred_2) (pred_1 tick) (tock pred_2)

7.6.5.2. 更多示例🔗

由全可达性推出的无限链

一个关系的自反传递闭包可以用归纳方式刻画:

inductive Star (R : α α Prop) : α α Prop where | refl : x : α, Star R x x | step : x y z, R x y Star R y z Star R x z

无限序列则用余归纳方式刻画:

def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint

如果从起始状态 a 出发,经由自反传递闭包可达的每个状态都有后继,那么从 a 出发就存在一条无限链。 谓词 AllSeqInf 表示每个可达状态都有后继:

def AllSeqInf (R : α α Prop) (x : α) : Prop := y : α, Star R x y z, R y z

证明这件事蕴含存在无限链,可以通过余归纳完成:

theorem infSeq_of_allSeqInf (R : α α Prop) : x, AllSeqInf R x InfSeq R x := α:Sort u_1R:α α Prop (x : α), AllSeqInf R x InfSeq R x α:Sort u_1R:α α Prop (a : α), AllSeqInf R a b, R a b AllSeqInf R b α:Sort u_1R:α α Propx:αH:AllSeqInf R x b, R x b AllSeqInf R b α:Sort u_1R:α α Propx:αH: (y : α), Star R x y z, R y z b, R x b AllSeqInf R b α:Sort u_1R:α α Propx:αH: (y : α), Star R x y z, R y zH': z, R x z b, R x b AllSeqInf R b α:Sort u_1R:α α Propx:αH: (y : α), Star R x y z, R y zy:αRxy:R x y b, R x b AllSeqInf R b All goals completed! 🐙
到传递闭包为止的余归纳

一个强化后的余归纳原理允许把余归纳假设应用到传递闭包为止。 给定一个谓词 X,若每个 X-状态都能经过一步或多步 R 迁移到另一个 X-状态,那么每个 X-状态都满足 InfSeq R

inductive Star (R : α α Prop) : α α Prop where | refl : x : α, Star R x x | step : x y z, R x y Star R y z Star R x z def InfSeq (R : α α Prop) (a : α) : Prop := b, R a b InfSeq R b coinductive_fixpoint variable {α : Sort _} {R : α α Prop} inductive Plus (R : α α Prop) : α α Prop where | left : a b c, R a b Star R b c Plus R a c theorem plusStar (a b : α) : Plus R a b Star R a b := α:Sort u_1R:α α Propa:αb:αPlus R a b Star R a b α:Sort u_1R:α α Propa:αb:αh:Plus R a bStar R a b; α:Sort u_1R:α α Propa:αb:αb✝:αa✝¹:R a b✝a✝:Star R b✝ bStar R a b case left _ h₂ h₃ α:Sort u_1R:α α Propa:αb:αb✝:αh₂:R a b✝h₃:Star R b✝ bStar R a b All goals completed! 🐙 theorem plusStarTrans (a b c : α) : Star R a b Plus R b c Plus R a c := α:Sort u_1R:α α Propa:αb:αc:αStar R a b Plus R b c Plus R a c α:Sort u_1R:α α Propa:αb:αc:αs:Star R a bp:Plus R b cPlus R a c; α:Sort u_1R:α α Propa:αb:αc:αx✝:αp:Plus R x✝ cPlus R x✝ cα:Sort u_1R:α α Propa:αb:αc:αx✝:αy✝:αz✝:αa✝¹:R x✝ y✝a✝:Star R y✝ z✝a_ih✝:Plus R z✝ c Plus R y✝ cp:Plus R z✝ cPlus R x✝ c case refl α:Sort u_1R:α α Propa:αb:αc:αx✝:αp:Plus R x✝ cPlus R x✝ c All goals completed! 🐙 case step d e _ rel _ ih α:Sort u_1R:α α Propa:αb:αc:αd:αe:αz✝:αrel:R x✝ y✝a✝:Star R y✝ z✝ih:Plus R z✝ c Plus R y✝ cp:Plus R z✝ cPlus R x✝ c All goals completed! 🐙 variable (X : α Prop) theorem infSeqCoinductionUpTo : ( (a : α), X a b, Plus R a b X b) (a : α), X a InfSeq R a := α:Sort u_1R:α α PropX:α Prop(∀ (a : α), X a b, Plus R a b X b) (a : α), X a InfSeq R a α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aInfSeq R a α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X a (a : α), ( b, Star R a b X b) b, R a b b_1, Star R b b_1 X b_1α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X a b, Star R a b X b case x α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X a b, Star R a b X b α:Sort u_1R:α α PropX:α Proph₁✝: (a : α), X a b, Plus R a b X ba:αrel:X aa':αh₁:Plus R a a'h₂:X a' b, Star R a b X b All goals completed! 🐙 case hyp α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X a (a : α), ( b, Star R a b X b) b, R a b b_1, Star R b b_1 X b_1 α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aa0:αa1:αh₃:Star R a0 a1h₄:X a1 b, R a0 b b_1, Star R b b_1 X b_1 α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aa0:αa1:αh₃:Star R a0 a1h₄:X a1mid:αh₅:Plus R a1 midh₆:X mid b, R a0 b b_1, Star R b b_1 X b_1 α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aa0:αa1:αh₃:Star R a0 a1h₄:X a1mid:αh₅:Plus R a1 midh₆:X midt:Plus R a0 mid b, R a0 b b_1, Star R b b_1 X b_1 α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aa0:αa1:αh₃:Star R a0 a1h₄:X a1mid:αh₅:Plus R a1 midh₆:X midb✝:αa✝¹:R a0 b✝a✝:Star R b✝ mid b, R a0 b b_1, Star R b b_1 X b_1 case left mid2 rel2 s α:Sort u_1R:α α PropX:α Proph₁: (a : α), X a b, Plus R a b X ba:αrel:X aa0:αa1:αh₃:Star R a0 a1h₄:X a1mid:αh₅:Plus R a1 midh₆:X midmid2:αrel2:R a0 b✝s:Star R b✝ mid b, R a0 b b_1, Star R b b_1 X b_1 All goals completed! 🐙

7.6.5.3. coinductive 命令🔗

Lean.Parser.Command.declaration : commandcoinductive 命令提供一种定义余归纳谓词的语法,其形式与 Lean.Parser.Command.declaration : commandinductive 声明的语法相仿。 无需使用 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 编写递归函数,而是像归纳类型那样以构造器来编写声明。

语法余归纳谓词
command ::= ...
    | `declModifiers` 是声明修饰符的集合,包括:

* 文档注释 `/-- ... -/`
* 属性列表 `@[attr1, attr2]`
* 可见性说明符 `private` 或 `public`
* `protected`
* `noncomputable`
* `unsafe`
* `partial` 或 `nonrec`

所有修饰符都是可选的,并且必须按上述顺序出现。
`nestedDeclModifiers` 与 `declModifiers` 相同,但属性与声明打印在同一行;它用于嵌套在其他语法中的声明,例如结构体字段。coinductive `declId` 匹配 `foo` 或 `foo.{u,v}`:一个标识符,后面可以跟一个宇宙名称列表。declId `optDeclSig` 匹配类型可选的声明签名:先是一列绑定器,随后可以有 `: type`。(ident | A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context.
For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`.

The way this works is that holes create fresh metavariables.
The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities.
This is often known as *unification*.

Normally, all holes must be solved for. However, there are a few contexts where this is not necessary:
* In `match` patterns, holes are catch-all patterns.
* In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals.

Related concept: implicit parameters are automatically filled in with holes during the elaboration process.

See also `?m` syntax (synthetic holes).
hole | bracketedBinder)* : term where
        ctor*

Lean.Parser.Command.declaration : commandcoinductive 命令通过指定构造器来定义余归纳谓词。 它只能用于定义谓词,即取值于 Prop 的类型。

Lean.Parser.Command.declaration : commandcoinductive 命令定义的谓词与对应的 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 定义相同。 此外,它还会生成构造器和分情况分析原理,很像普通的 Lean.Parser.Command.declaration : commandinductive 声明。

通过 coinductive 定义余归纳谓词

前述示例中的谓词 InfSeq 也可以等价地使用 Lean.Parser.Command.coinductivecoinductive 命令定义:

variable (α : Type) coinductive InfSeq (r : α α Prop) : α Prop where | step : r a b InfSeq r b InfSeq r a

这会生成一个构造器和一个余归纳原理

InfSeq.step (α : Type) (r : α α Prop) {a b : α} : r a b InfSeq α r b InfSeq α r aInfSeq.coinduct (α : Type) (r : α α Prop) (pred : α Prop) : ( (a : α), pred a b, r a b pred b) (a : α), pred a InfSeq α r a

还会生成一个分情况分析原理:

InfSeq.casesOn (α : Type) (r : α α Prop) {motive : (a : α) InfSeq α r a Prop} {a : α} (t : InfSeq α r a) : ( {a b} (a_1 : r a b) (a_2 : InfSeq α r b), motive a (InfSeq.step α r a_1 a_2)) motive a t

在证明中,可以通过 cases 策略使用分情况分析:

theorem InfSeq.casesOnTest (r : α α Prop) (a : α) : InfSeq α r a b, r a b := α:Typer:α α Propa:αInfSeq α r a b, r a b α:Typer:α α Propa:αh:InfSeq α r a b, r a b α:Typer:α α Propa:αb✝:αa✝¹:InfSeq α r b✝a✝:r a b✝ b, r a b case step b _ hr α:Typer:α α Propa:αb:αa✝:InfSeq α r b✝hr:r a b✝ b, r a b All goals completed! 🐙

7.6.5.3.1. 精译🔗

在底层,Lean.Parser.Command.declaration : commandcoinductive 命令会分若干步进行精译。 首先,将其当作普通的 Lean.Parser.Command.declaration : commandinductive 声明进行处理。 不过,在向内核注册类型之前,会创建一个平坦归纳类型(也称为函子):构造器前提中余归纳谓词的每次递归出现都会替换为一个显式参数。

平坦归纳类型

此示例使用无限序列的余归纳规约:

coinductive InfSeq (r : α α Prop) : α Prop where | step : r a b InfSeq r b InfSeq r a

对于 InfSeq,生成的平坦归纳类型为:

InfSeq._functor : (α : Type) (α α Prop) (α Prop) α Prop

其构造器使用谓词参数取代递归调用:

set_option pp.proofs true in inductive InfSeq._functor : (α : Type) (α α Prop) (α Prop) α Prop number of parameters: 3 constructors: InfSeq._functor.step : (α : Type) (r : α α Prop) (InfSeq._functor.call : α Prop) {a b : α}, r a b InfSeq._functor.call b InfSeq._functor α r InfSeq._functor.call a#print InfSeq._functor
inductive InfSeq._functor : (α : Type)  (α  α  Prop)  (α  Prop)  α  Prop
number of parameters: 3
constructors:
InfSeq._functor.step :  (α : Type) (r : α  α  Prop) (InfSeq._functor.call : α  Prop) {a b : α},
  r a b  InfSeq._functor.call b  InfSeq._functor α r InfSeq._functor.call a

随后构造等价的存在形式,将每个构造器表示为依赖积(即存在量词与合取)的析取。 此形式用于单调性检查以及生成易读的余归纳原理。

存在形式
coinductive InfSeq (r : α α Prop) : α Prop where | step : r a b InfSeq r b InfSeq r a set_option pp.proofs true in def InfSeq._functor.existential : (α : Type) (α α Prop) (α Prop) α Prop := fun α r InfSeq._functor.call a => b, r a b InfSeq._functor.call b#print InfSeq._functor.existential
def InfSeq._functor.existential : (α : Type)  (α  α  Prop)  (α  Prop)  α  Prop :=
fun α r InfSeq._functor.call a =>  b, r a b  InfSeq._functor.call b

这两种形式由一个等价定理联系起来:

InfSeq._functor.existential_equiv : (α : Type) (r : α α Prop) (InfSeq._functor.call : α Prop) (a : α), InfSeq._functor α r InfSeq._functor.call a b, r a b InfSeq._functor.call b#check @InfSeq._functor.existential_equiv
InfSeq._functor.existential_equiv :  (α : Type) (r : α  α  Prop) (InfSeq._functor.call : α  Prop) (a : α),
  InfSeq._functor α r InfSeq._functor.call a   b, r a b  InfSeq._functor.call b

随后,使用偏不动点机制和 Lean.Order.ReverseImplicationOrder 完备格实例,将存在形式注册为余归纳谓词。 利用平坦归纳类型与存在形式之间的对应关系,系统会像处理普通归纳类型一样生成构造器和分情况分析消去器。

对于名为 P 的余归纳谓词,会生成以下声明:

  • P._functor平坦归纳类型

  • P._functor.existential存在形式

  • P._functor.existential_equiv:两种形式之间的等价定理

  • P.functor_unfold:联系余归纳谓词与其平坦归纳类型的定理

  • 构造器(例如 P.step):与声明中的各构造器相对应

  • P.casesOn:分情况分析原理

  • P.coinduct余归纳原理

7.6.5.3.2. 余归纳与归纳互递归块🔗

在包含 Lean.Parser.Command.coinductivecoinductive 定义的互递归块中,Lean.Parser.Command.inductiveIn Lean, every concrete type other than the universes and every type constructor other than dependent arrows is an instance of a general family of type constructions known as inductive types. It is remarkable that it is possible to construct a substantial edifice of mathematics based on nothing more than the type universes, dependent arrow types, and inductive types; everything else follows from those. Intuitively, an inductive type is built up from a specified list of constructors. For example, `List α` is the list of elements of type `α`, and is defined as follows: ``` inductive List (α : Type u) where | nil | cons (head : α) (tail : List α) ``` A list of elements of type `α` is either the empty list, `nil`, or an element `head : α` followed by a list `tail : List α`. See [Inductive types](https://lean-lang.org/theorem_proving_in_lean4/inductive_types.html) for more information. inductive 关键字会被重新解释:它不会注册为普通的内核归纳类型,而是通过格理论的归纳不动点机制进行精译。 这允许在同一互递归块中混合余归纳与归纳谓词。

余归纳—归纳互递归块

谓词 TickTock 互相定义,其中 Tick 是余归纳谓词,Tock 是归纳谓词:

mutual coinductive Tick : Prop where | mk : ¬Tock Tick inductive Tock : Prop where | mk : ¬Tick Tock end

两个构造器都可用:

Tick.mk : ¬Tock Tick#check @Tick.mk
Tick.mk : ¬Tock  Tick
Tock.mk : ¬Tick Tock#check @Tock.mk
Tock.mk : ¬Tick  Tock

系统会生成一个互归纳原理:

Tick.mutual_induct : (pred_1 pred_2 : Prop), (pred_1 pred_2 False) ((pred_1 False) pred_2) (pred_1 Tick) (Tock pred_2)#check @Tick.mutual_induct
Tick.mutual_induct :  (pred_1 pred_2 : Prop),
  (pred_1  pred_2  False)  ((pred_1  False)  pred_2)  (pred_1  Tick)  (Tock  pred_2)

7.6.5.3.3. 限制🔗

Lean.Parser.Command.declaration : commandcoinductive 命令有以下限制:

  • 它只能定义谓词,即取值于 Prop 的类型。 尝试在 Type 或更高宇宙中定义余归纳类型会导致错误。

  • 正在定义的谓词不能带有宏作用域

  • 尚不支持通过 Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match 进行模式匹配;请改用 cases 策略。

仅限谓词

尝试定义一个并非谓词的余归纳类型会导致错误:

coinductive `coinductive` keyword can only be used to define predicatesMyNat where | zero : MyNat | succ : MyNat MyNat
`coinductive` keyword can only be used to define predicates

7.6.5.4. 理论与构造🔗

余归纳与归纳谓词的构造建立在完备格上的 Knaster–Tarski 不动点定理之上。 偏不动点递归依赖链完备偏序(Lean.Order.CCPO),而余归纳与归纳谓词使用更强的完备格概念。

关键思想是,Prop 带有一个按蕴涵排序的完备格结构(当 P → QP ⊑ Q);根据 Knaster–Tarski 定理,完备格上的任意单调自映射同时具有最小与最大不动点。 余归纳谓词使用反向蕴涵序(当 Q → PP ⊑ Q),因此该反向序中的最小不动点就是标准序中的最大不动点。 对于形如 α → Prop 的谓词,将此格结构逐点提升到函数类型即可提供所需环境。 对于互递归块,完备格的积仍是完备格。 该构造与偏不动点机制共享内部实现。

7.6.5.4.1. 完备格🔗

完备格是一种偏序,其中每个子集(而不仅是每条链)都有最小上界。

🔗类型类
Lean.Order.CompleteLattice.{u} (α : Sort u) : Sort (max 1 u)
Lean.Order.CompleteLattice.{u} (α : Sort u) : Sort (max 1 u)

完备格是一种偏序,其中每个子集都有最小上界。

Lean.Order.CompleteLattice.mk.{u}
rel : α  α  Prop

继承自父结构。

rel_refl :  {x : α}, x  x

继承自父结构。

rel_trans :  {x y z : α}, x  y  y  z  x  z

继承自父结构。

rel_antisymm :  {x y : α}, x  y  y  x  x = y

继承自父结构。

has_sup :  (c : α  Prop), Exists (is_sup c)

任意子集的最小上界都存在。

每个完备格都会给出一个链完备偏序,因为每条链尤其也是一个子集;但反过来一般并不成立。 例如,居留类型上的平坦序(偏不动点用于尾递归函数)是链完备偏序,却不是完备格。

根据 Knaster–Tarski 定理,在完备格中,单调函数的最小不动点可以直接构造为所有前不动点的下确界:

🔗定义
Lean.Order.lfp.{u} {α : Sort u} [CompleteLattice α] (f : α α) : α
Lean.Order.lfp.{u} {α : Sort u} [CompleteLattice α] (f : α α) : α

函数 f 的最小不动点,即所有前不动点的下确界。

🔗定理
Lean.Order.lfp_fix.{u} {α : Sort u} [CompleteLattice α] {f : α α} (hm : monotone f) : lfp f = f (lfp f)
Lean.Order.lfp_fix.{u} {α : Sort u} [CompleteLattice α] {f : α α} (hm : monotone f) : lfp f = f (lfp f)

单调函数 f 的最小不动点确实是不动点。

对应的归纳原理是 Park 归纳:要证明某个性质对最小不动点的所有元素成立,只需证明应用一次定义函数会保持该性质。

🔗定理
Lean.Order.lfp_le_of_le_monotone.{u} {α : Sort u} [CompleteLattice α] (f : α α) {hm : monotone f} (x : α) : f x x lfp_monotone f hm x
Lean.Order.lfp_le_of_le_monotone.{u} {α : Sort u} [CompleteLattice α] (f : α α) {hm : monotone f} (x : α) : f x x lfp_monotone f hm x

单调函数 f 的最小不动点所满足的 Park 归纳原理。 此定理显式接受一个 f 单调的见证。

7.6.5.4.2. 命题上的格结构🔗

类型 Prop 具有两种自然的完备格结构,分别产生不同种类的不动点:

  • Lean.Order.ImplicationOrder 按蕴涵对命题排序:P ⊑ Q 意味着 P → Q。 该序中的最小不动点给出在定义规则下闭合的最小谓词,对应于归纳谓词。 这是 Lean.Parser.Command.declaration : commandinductive_fixpoint 使用的序。

  • Lean.Order.ReverseImplicationOrder 按反向蕴涵对命题排序:P ⊑ Q 意味着 Q → P。 该反向序中的最小不动点是标准序中的最大不动点,由此得到与定义规则相容的最大谓词。 这对应于余归纳谓词。 这是 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 使用的序。

以完备格为值域的箭头类型继承完备格结构,完备格的积也是完备格。 这些闭包性质使该构造能够扩展到任意元数的谓词和互递归块。

7.6.5.4.3. 单调性🔗

将谓词定义为不动点,要求定义方程相对于适当的序是单调的。 对于 Lean.Parser.Command.declaration : commandcoinductive 命令,以及 Lean.Parser.Command.declaration : commandcoinductive_fixpointLean.Parser.Command.declaration : commandinductive_fixpoint 终止子句,单调性要求都是语义上的,而非语法上的。 monotonicity 策略通过组合以 partial_fixpoint_monotone 属性注册的引理来证明单调性。 这种方法比严格正性更宽松。 例如,通过在 Lean.Order.ImplicationOrderLean.Order.ReverseImplicationOrder 之间翻转序,可以正确处理否定和蕴涵。 这正是同一互递归块中能够混合归纳与余归纳不动点的原因。

monotonicity 策略所能处理的构造是可扩展的:注册额外的 partial_fixpoint_monotone 引理,可让该策略学会处理新的逻辑联结词或高阶函数。 或者,在使用 Lean.Parser.Command.declaration : commandcoinductive_fixpoint 时,可以通过 monotonicity 子句提供显式单调性证明项。

已注册单调性引理的完整列表以及单调性策略的更多细节,请参阅偏不动点的理论一节

7.6.6. 偏定义与不安全定义🔗

大多数 Lean 函数既可在 Lean 的类型论中进行推理,也可被编译并运行;但凡被标记为 partialunsafe 的定义,则无法在逻辑层面进行有意义的推理。 从逻辑视角看,partial 函数是不透明常量;而凡是引用 unsafe 定义的定理都会被直接拒绝。 作为无法用于推理的交换条件,这些定义受到的约束大幅减少:这使得一些原本不切实际或成本过高而难以给出证明的程序仍然可以编写,同时又不牺牲其余部分的形式化推理。 本质上,Lean 的 partial 子集是一种传统的函数式编程语言,但与定理证明功能深度集成;而 unsafe 子集则在少数情形下允许打破 Lean 的运行时不变式,但相应地与定理证明功能的集成程度较低。 类似地,noncomputable 定义可以使用在程序中不合语义、但在逻辑中有意义的特性。

7.6.6.1. 偏函数🔗

partial 修饰符只能用于函数定义。 偏函数无需展示终止性,Lean 也不会尝试证明它终止。 之所以称为“偏”,是因为它们未必为定义域中的每个元素指定到余域元素的映射:对某些(乃至所有)输入,它们可能无法终止。 这类定义会被精译为包含显式递归的 预定义 并由内核进行类型检查;不过在逻辑层面它们随后会被当作不透明常量。

函数的返回类型必须是可被占据的;这可确保自洽性。 否则,偏函数就可能拥有诸如 Unit Empty 的类型。 结合 Empty.elim,即便该函数并不归约,也可以据此“证明” False

对于偏定义,内核负责以下检查:

  • 确认预定义的类型确为一个良构类型;

  • 确认预定义的类型是函数类型;

  • 通过需求 NonemptyInhabited 实例,确保函数的余域是可被占据的;

  • 在“假设 Lean 拥有递归定义”的前提下,检查生成项会通过类型检查。

尽管递归定义不是内核类型论的一部分,仍然可以用内核来检查定义体是否具有正确的类型。 其工作方式与其他函数式语言相同:在一个“该定义已与其类型绑定”的环境中检查定义体,从而为递归的使用做类型检查。 一旦确认通过类型检查,定义体会被丢弃,内核仅保留那个不透明常量。 与所有 Lean 函数一样,编译器会基于精译得到的 预定义 生成代码。

即便内核不会对偏函数展开,仍可以在不依赖其具体实现的前提下,对调用它们的其他函数开展推理。

证明中的偏函数

递归函数 nextPrime 通过对候选数做试除测试来计算给定数之后的下一个素数,这样的做法效率不高。 由于素数是无限多的,它总是会终止;然而要正式给出这一点的证明并不容易,因此它被标记为 partial

def isPrime (n : Nat) : Bool := Id.run do for i in [2:n] do if i * i > n then return true if n % i = 0 then return false return true partial def nextPrime (n : Nat) : Nat := let n := n + 1 if isPrime n then n else nextPrime n

尽管如此,仍然可以证明下面两个函数是相等的:

def answerUser (n : Nat) : String := s!"The next prime is {nextPrime n}" def answerOtherUser (n : Nat) : String := " ".intercalate [ "The", "next", "prime", "is", toString (nextPrime n) ]

事实上,该证明只需使用 rfl

theorem answer_eq_other : answerUser = answerOtherUser := answerUser = answerOtherUser All goals completed! 🐙

7.6.6.2. 不安全定义🔗

不安全定义的保障比偏函数更少。 它们的余域不必是可被占据的,且不限于函数定义;同时还能使用一些可能违反内部不变式或破坏抽象的 Lean 特性。 因此,它们完全不能用作数学推理的一部分。

类型论会把偏函数当作不透明常量处理;而不安全定义只能被其他不安全定义引用。 因此,任何调用了不安全函数的函数本身也必须是不安全的;定理则不允许被声明为不安全。

除了不受限制地使用递归之外,不安全函数还能在类型间强制转换、检查两个值是否为内存中的同一对象、读取指针值、以及在原本纯净的代码中运行 IO 动作。 使用这些算子需要对 Lean 的实现有深入理解。

🔗不安全定义
unsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : β
unsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : β

此函数把类型 α 的值强制转换为类型 β,在编译器中不执行任何操作。它是 极其危险的:无法保证 αβ 具有相同的数据表示,因而可能导致内存不安全; 它在逻辑上也不可靠,因为可以直接把 True 强制转换为 False。出于这些原因, 此函数被标记为 unsafe

其实现先把 αβ 提升到同一个宇宙,再使用 cast (lcProof : ULift (PLift α) = ULift (PLift β)) 实际执行强制转换。 这些操作在编译器中全都不执行任何操作。

正确使用此函数需要了解源类型和目标类型的数据表示。以下几类强制转换在当前运行时中 是安全的:

  • αβ 的表示兼容时,从 Array αArray β;更一般地,其他归纳类型亦然。

  • Quot α rα 之间。

  • @Subtype α pα 之间;更一般地,只含一个类型为 α 的非 Prop 字段的任何结构体亦然。

  • α 是装箱的泛型类型时,在 αNonScalar 之间转换;所谓装箱的泛型类型, 是指接受任意类型 α、且不会特化为 UInt8 等标量类型的函数所处理的类型。

🔗不安全定义
ptrEq.{u_1} {α : Type u_1} (a b : α) : Bool
ptrEq.{u_1} {α : Type u_1} (a b : α) : Bool

比较两个对象的指针是否相等。

若两个对象在运行时恰好分配在同一地址上,则它们的指针相等。此函数是不安全的, 因为它能够区分定义相等的值。

🔗不安全定义
ptrEqList.{u_1} {α : Type u_1} (as bs : List α) : Bool
ptrEqList.{u_1} {α : Type u_1} (as bs : List α) : Bool

逐元素比较两个对象列表的指针是否相等。当两个列表长度相同,且对应索引处对象的 指针都相等时,返回 true

若两个对象在运行时恰好分配在同一地址上,则它们的指针相等。此函数是不安全的, 因为它能够区分定义相等的值。

🔗不安全不透明定义
ptrAddrUnsafe.{u} {α : Type u} (a : α) : USize
ptrAddrUnsafe.{u} {α : Type u} (a : α) : USize

返回对象被分配到的地址。

此函数是不安全的,因为它能够区分定义相等的值。

🔗不安全不透明定义
isExclusiveUnsafe.{u} {α : Type u} (a : α) : Bool
isExclusiveUnsafe.{u} {α : Type u} (a : α) : Bool

a 是独占对象,则返回 true

对象为单线程使用且其引用计数为 1 时,该对象是独占的。此函数是不安全的,因为它 能够区分定义相等的值。

🔗不安全定义
unsafeIO {α : Type} (fn : IO α) : Except IO.Error α
unsafeIO {α : Type} (fn : IO α) : Except IO.Error α

在纯上下文中执行任意副作用,并通过 Except 表示异常。这是一项危险操作, 很容易破坏 Lean 程序含义所依赖的重要假设。只有在透彻理解编译器内部机制、并且 仅用于实现观察上纯净的操作时,才应极其谨慎地使用它。

此函数并不是把 EIO ε αIO α 转换为 α 的好方法;应改用 do 记法

由于所得值会被视为无副作用的项,编译器可能对该函数的调用重新排序、复制或删除。 副作用甚至可能被提升到常量的初始化过程中,因此即使原本永远不会调用,也可能在 初始化时发生。

🔗不安全定义
unsafeEIO {ε α : Type} (fn : EIO ε α) : Except ε α
unsafeEIO {ε α : Type} (fn : EIO ε α) : Except ε α

在纯上下文中执行任意副作用,并通过 Except 表示异常。这是一项危险操作, 很容易破坏 Lean 程序含义所依赖的重要假设。只有在透彻理解编译器内部机制、并且 仅用于实现观察上纯净的操作时,才应极其谨慎地使用它。

此函数并不是把 EIO ε αIO α 转换为 α 的好方法;应改用 do 记法

由于所得值会被视为无副作用的项,编译器可能对该函数的调用重新排序、复制或删除。 副作用甚至可能被提升到常量的初始化过程中,因此即使原本永远不会调用,也可能在 初始化时发生。

🔗不安全定义
unsafeBaseIO {α : Type} (fn : BaseIO α) : α
unsafeBaseIO {α : Type} (fn : BaseIO α) : α

在纯上下文中执行任意副作用。这是一项危险操作,很容易破坏 Lean 程序含义所 依赖的重要假设。只有在透彻理解编译器内部机制、并且仅用于实现观察上纯净的操作时, 才应极其谨慎地使用它。

此函数并不是把 BaseIO α 转换为 α 的好方法;应改用 do 记法

由于所得值会被视为无副作用的项,编译器可能对该函数的调用重新排序、复制或删除。 副作用甚至可能被提升到常量的初始化过程中,因此即使原本永远不会调用,也可能在 初始化时发生。

不安全算子经常被用来利用底层细节编写高性能代码。 类似于通过 FFI 在运行时用 C 代码替换 Lean 代码的方式, 也可以在运行时程序中用不安全 Lean 代码替换安全 Lean 代码。 这可以通过在待替换的函数(通常是 opaque 定义)上添加 implemented_by 属性来实现。 这并不会威胁 Lean 作为逻辑的自洽性:被替换的常量已通过内核检查,而不安全替代仅用于运行时代码。 但这仍然是有风险的——无论是 C 代码还是不安全代码,都可能执行任意副作用。

属性替换运行时实现

implemented_by 属性指示编译器在已编译代码中将某个常量替换为另一个常量。 被替换上去的常量可以是不安全的。

attr ::= ...
    | implemented_by ident
使用指针检查相等性

通常,BEq 实例的相等判定需要完全遍历两个参数以判断它们是否相等。 如果它们其实就是内存中的同一个对象,这样的遍历就显得很浪费。 在遍历之前先做一次指针相等性测试,可以尽早捕获这种情况。

比较的类型是 Tree(二叉树):

inductive Tree α where | empty | branch (left : Tree α) (val : α) (right : Tree α)

一个不安全函数可以用指针相等来更快地结束结构相等性测试;当指针不相等时,再回退到结构检查:

unsafe def Tree.fastBEq [BEq α] (t1 t2 : Tree α) : Bool := if ptrEq t1 t2 then true else match t1, t2 with | .empty, .empty => true | .branch l1 x r1, .branch l2 y r2 => if ptrEq x y || x == y then l1.fastBEq l2 && r1.fastBEq r2 else false | _, _ => false

在一个不透明定义上添加 implemented_by 属性,就能在安全与不安全代码之间搭桥:

@[implemented_by Tree.fastBEq] opaque Tree.beq [BEq α] (t1 t2 : Tree α) : Bool instance [BEq α] : BEq (Tree α) where beq := Tree.beq
利用运行时表示

由于 Fin 与其底层的 Nat 具有相同的运行时表示,List.map Fin.val 可以用 unsafeCast 来替换,从而避免一次在实践中“什么也没做”的线性时间遍历:

unsafe def unFinImpl (xs : List (Fin n)) : List Nat := unsafeCast xs @[implemented_by unFinImpl] def unFin (xs : List (Fin n)) : List Nat := xs.map Fin.val

从 Lean 内核的视角看,unFin 是用 List.map 定义的:

theorem unFin_length_eq_length {xs : List (Fin n)} : (unFin xs).length = xs.length := n:Natxs:List (Fin n)(unFin xs).length = xs.length All goals completed! 🐙

在已编译代码中,则不会发生对该列表的遍历。

这种替换方式具有风险:证明与已编译代码之间的一致性完全依赖于两个实现的等价性,而这点无法在 Lean 中证明。 这种一致性依赖 Lean 实现层面的细节。 这些“逃逸舱门”应当非常谨慎地使用。

7.6.7. 控制归约🔗

在检查证明与程序时,Lean 会考虑 可约性,它也称为透明性。 定义的可约性决定精译和证明执行过程中可以在哪些上下文展开它。

可约性分为五个等级:

不可约

在精译过程中,不可约定义完全不会被展开。 对定义应用 irreducible 属性可使其不可约。

半可约

半可约定义不会被类型类实例合成或 simp 等潜在代价较高的自动化过程展开,但在检查定义相等性和解析广义字段记法时会展开。 Lean.Parser.Command.declaration : commanddef 命令通常创建半可约定义,除非属性指定了不同等级;不过,采用良基递归的定义默认不可约。

隐式参数可约

检查函数隐式实参的定义相等性时,会展开隐式参数可约的定义。 这里的隐式实参包括普通隐式实参、实例隐式实参和严格隐式实参。 如果某个定义出现在隐式实参的类型中,并且预期它能够归约,就应将其设为隐式参数可约。

实例可约

类型类实例合成期间会展开实例可约的定义。 所有类型类实例都应当是实例可约或完全可约的。 由 Lean.Parser.Command.instanceinstance 命令创建的实例会自动标记为实例可约。

可约

可约定义几乎会在所有场合按需展开。 类型类实例合成、定义相等性检查以及语言的其余部分,基本都会把这种定义视作缩写。 Lean.Parser.Command.declaration : commandabbrev 命令创建的定义采用这一等级。

可约性与实例合成

下面这三个 String 的别名分别是可约、半可约与不可约:

abbrev Phrase := String def Clause := String @[irreducible] def Utterance := String

在精译器进行定义相等检查时,可约与半可约别名会被展开,从而被视为与 String 等价:

def hello : Phrase := "Hello" def goodMorning : Clause := "Good morning"

相对地,不可约别名不会在定义相等测试中被展开,因此作为字符串的类型会被拒绝:

def goodEvening : Utterance := Type mismatch "Good evening" has type String but is expected to have type Utterance"Good evening"
Type mismatch
  "Good evening"
has type
  String
but is expected to have type
  Utterance

由于 Phrase 是可约的,ToString String 实例可被当作 ToString Phrase 实例来用:

instToStringString#synth ToString Phrase

然而 Clause 是半可约的,因此不能直接使用 ToString String 实例:

failed to synthesize ToString Clause Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.#synth ToString Clause
failed to synthesize
  ToString Clause

Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command.

可以显式启用该实例:构造一个会化简为 ToString String 实例的 ToString Clause 实例。 该示例之所以可行,是因为在进行定义相等检查时会展开半可约定义:

instance : ToString Clause := inferInstanceAs (ToString String)
可约性与广义字段记法

在查找匹配名称时,广义字段记法 会展开可约与半可约的声明。 给定 List 的一个半可约别名 Sequence

def Sequence := List def Sequence.ofList (xs : List α) : Sequence α := xs

广义字段记法允许从类型为 Sequence Nat 的项上访问 List.reverse

let xs := Sequence.ofList [1, 2, 3]; List.reverse xs : List Nat#check let xs : Sequence Nat := .ofList [1,2,3]; xs.reverse

然而,一旦将 Sequence 声明为不可约,就会阻止展开:

attribute [irreducible] Sequence let xs := Sequence.ofList [1, 2, 3]; sorry : ?m.13#check let xs : Sequence Nat := .ofList [1,2,3]; xs.Invalid field `reverse`: The environment does not contain `Sequence.reverse`, so it is not possible to project the field `reverse` from an expression xs of type `Sequence Nat`reverse
Invalid field `reverse`: The environment does not contain `Sequence.reverse`, so it is not possible to project the field `reverse` from an expression
  xs
of type `Sequence Nat`
属性可约性标注

可以使用如下五种可约性属性之一来设置某个定义的可约性:

attr ::= ...
    | reducible
attr ::= ...
    | instance_reducible
attr ::= ...
    | implicit_reducible
attr ::= ...
    | semireducible
attr ::= ...
    | irreducible

这些属性只能在被修改定义所在的同一文件中全局应用;不过,它们也可以在任意位置以 Lean.Parser.Term.attrKindlocal 方式应用。

7.6.7.1. 可约性与策略🔗

下面这些策略可控制大多数策略会展开哪些定义:with_reduciblewith_reducible_and_instanceswith_unfolding_all

可约性与策略

函数 plussumtally 都是 Nat.add 的同义名,且分别为可约、半可约与不可约:

abbrev plus := Nat.add def sum := Nat.add @[irreducible] def tally := Nat.add

可约同义名会被 simp 展开:

theorem plus_eq_add : plus x y = x + y := x:Naty:Natplus x y = x + y All goals completed! 🐙

半可约同义名则不会被 simp 展开:

theorem sum_eq_add : sum x y = x + y := x:Naty:Natsum x y = x + y `simp` made no progressx:Naty:Natsum x y = x + y

不过,由 rfl 触发的定义相等检查会展开 sum

theorem sum_eq_add : sum x y = x + y := x:Naty:Natsum x y = x + y All goals completed! 🐙

不可约的 tally 不会被定义相等所化简。

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y Tactic `rfl` failed: The left-hand side tally x y is not definitionally equal to the right-hand side x + y x y:Nattally x y = x + yx:Naty:Nattally x y = x + y

当显式提供时,simp 可以展开任意定义,甚至包括不可约的:

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y All goals completed! 🐙

类似地,可将证明的一部分放入 with_unfolding_all 块中以忽略不可约性:

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y with_unfolding_all All goals completed! 🐙
可约性与隐式实参

函数 plussumtally 都是 Nat.add 的同义名,且分别为可约、实例可约与不可约:

abbrev plus := Nat.add @[instance_reducible] def sum := Nat.add def tally := Nat.add

Nonzero 的实例包含一个给定数字不等于零的证明。 函数 notZero 从合成得到的实例中提取该证明:

class Nonzero (n : Nat) where non_zero : n 0 instance Nonzero.instSucc : Nonzero (n + 1) where non_zero := n:Natn + 1 0 All goals completed! 🐙 Definition `notZero` is a proposition; use `theorem` instead of `def` Note: This linter can be disabled with `set_option linter.defProp false`def notZero (n : Nat) [Nonzero n] : n 0 := Nonzero.non_zero

对于可约定义 plus,可以找到该实例:

notZero (plus 2 2) : plus 2 2 0#check notZero (plus 2 2)

对于实例可约定义 sum,同样可以找到该实例。 这是因为类型 Nonzero (sum 2 2)notZero 的一个 实例隐式参数的类型。 具体而言,sum 会归约为本身也是实例可约的 Nat.add,因此该类型会归约为 Nonzero 4

notZero (sum 2 2) : sum 2 2 0#check notZero (sum 2 2)

由于 tally 不会被归约,其实例合成会失败:

#check failed to synthesize instance of type class Nonzero (tally 2 2) Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.notZero (tally 2 2)
failed to synthesize instance of type class
  Nonzero (tally 2 2)

Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.

在其他上下文中,例如调用 simp 时,plus 会被展开:

theorem plus_eq_add : plus x y = x + y := x:Naty:Natplus x y = x + y All goals completed! 🐙

不过,实例可约的同义名不会被 simp 展开:

theorem sum_eq_add : sum x y = x + y := x:Naty:Natsum x y = x + y `simp` made no progressx:Naty:Natsum x y = x + y
`simp` made no progress

7.6.7.2. 修改可约性🔗

可以在定义所在的模块中,使用 Lean.Parser.Command.attribute : commandattribute 命令施加相应属性,从而全局修改某个定义的可约性。 在其他模块中,可通过带 local 修饰符的属性应用来修改已导入定义的可约性。 Lean.Parser.commandSeal__ : commandThe `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`. This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs. In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`. This attribute specifies that `foo` should be treated as irreducible only within the local scope, which helps in maintaining the desired abstraction level without affecting global settings. sealLean.Parser.commandUnseal__ : commandThe `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs. Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`. Applying this attribute makes `foo` semireducible only within the local scope. unseal 命令是该流程的便捷写法。

语法局部不可约性

seal foo 命令确保定义 foo 被封闭,即将其标记为 [irreducible]。当希望阻止 证明中的 foo 发生归约时,此命令尤其有用。

就功能而言,seal foo 等价于 attribute [local irreducible] foo。该属性规定 只在局部作用域内把 foo 视为不可约,从而既维持所需的抽象层次,又不影响全局设置。

command ::= ...
    | The `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`.
This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs.

In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`.
This attribute specifies that `foo` should be treated as irreducible only within the local scope,
which helps in maintaining the desired abstraction level without affecting global settings.
seal ident ident*
语法局部可约性

unseal foo 命令确保定义 foo 被解除封闭,即将其标记为 [semireducible],也就是 默认的可约性设置。需要在证明中允许 foo 进行一定程度的归约时,可以使用此命令。

就功能而言,unseal foo 等价于 attribute [local semireducible] foo。应用该属性 只会在局部作用域内把 foo 设为半可约。

command ::= ...
    | The `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the
default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs.

Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`.
Applying this attribute makes `foo` semireducible only within the local scope.
unseal ident ident*

7.6.7.3. 选项🔗

出于性能考虑,精译器与许多策略会构建索引与缓存。 其中不少会考虑可约性;而一旦全局改变了可约性,就无法使这些索引/缓存失效并重新生成。 默认情况下,会禁止对可约性进行可能带来不可预测结果的不安全修改;不过,可通过 allowUnsafeReducibility 选项启用之。

🔗选项
allowUnsafeReducibility

默认值:false

允许用户修改声明的可约性设置,即使这类修改被认为可能有危险。例如,simp 与类型类 解析会维护项索引,其中会展开可约声明;修改可约性可能使这些索引与缓存失效。

默认值为 false