h5i
本中文版由作者借助 AI 翻译而成。作者并非中文母语者,译文未经人工逐句校对,可能存在错误。如发现问题,请在 GitHub 上提交 issue。

第 2 章

项的语言

内核在检查任何东西之前,先需要一种精确的项语言。我们的语言只有五种项,这已足以表达带命题量化的命题逻辑。本章确定文法、全书沿用的记法约定,以及表示项的 Rust 类型。

五种项

核心语言是一个小型依赖类型论,它的项由如下文法生成

t,A,B  ::=  Sort u  ∣  x  ∣  Π(x:A). B  ∣  λ(x:A). t  ∣  t  st, A, B \;::=\; \Sort{u} \;\mid\; x \;\mid\; \tPi{x}{A}{B} \;\mid\; \tLam{x}{A}{t} \;\mid\; t\;s

其中 uu 是自然数,xx 是变量名。每种形式各有角色。

  • Sort u\Sort{u} 是宇宙(universe):一种元素本身也是类型的类型,数 uu 是它的层级,而 Sort 0\Sort{0} 是命题的宇宙。第 13 章解释为什么需要整个层级结构,而不是单个宇宙。
  • xx 是变量,它的含义来自所在的上下文:要么是包围它的某个 λ\lambda 或 Π\Pi 绑定子,要么是类型上下文中的一条假设。
  • Π(x:A). B\tPi{x}{A}{B} 是 Π\Pi 类型,即第 1 章的依赖函数类型,也就是这样一类函数的类型:接收一个 x:Ax : A,返回类型为 BB 的东西,而 BB 可以提到 xx。
  • λ(x:A). t\tLam{x}{A}{t} 是 λ\lambda 抽象,即接收类型为 AA 的参数 xx 并返回 tt 的函数。
  • t  st\;s 是 tt 对 ss 的应用。

文法不区分类型和项,命题 Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} 和它的证明 λ(P:Sort 0). λ(p:P). p\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}} 都是由同样五种构造子搭建的项:

λ(P:Sort 0). λ(p:P). p⏟proof:Π(P:Sort 0). P→P⏟proposition, the type of the proof.\underbrace{\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}}}_{\text{proof}} \quad : \quad \underbrace{\tPi{P}{\Sort{0}}{P \to P}}_{\text{proposition, the type of the proof}}.

把这两个项画成树,共用的文法就一目了然。每个节点都是五种构造子之一;Π\Pi 或 λ\lambda 节点上的名字是它绑定的变量。点击节点可以查看它代表的子项,也可以输入另一个项。

一个项是否为合法的类型,不是语法性质,而是由类型检查器判定的:一个项是类型,当且仅当它自己的类型是一个宇宙(第 5 章)。

约定

本书始终如一地使用以下字母。

字母 表示
s,t,a,b,fs, t, a, b, f 以及 A,B,C,TA, B, C, T(可带撇号或下标) 项
x,y,zx, y, z 以及任何由 Π\Pi 或 λ\lambda 绑定的名字 变量名
u,v,wu, v, w 宇宙层级
Γ\Gamma 类型上下文(第 3 章)
Σ\Sigma 常量签名(第 7 章)

三个缩写让项保持可读。

->。当 BB 不提到 xx 时,我们把 Π(x:A). B\tPi{x}{A}{B} 写作 A→BA \to B。于是普通函数类型是依赖函数类型的特例,蕴含是全称量化的特例。在实现中,-> 的绑定子是保留名 _。

结合性。应用向左结合,-> 向右结合:

f  a  b  =  (f  a)  b,A→B→C  =  A→(B→C).f\;a\;b \;=\; (f\;a)\;b, \qquad A \to B \to C \;=\; A \to (B \to C).

柯里化。两个参数的函数,就是接收第一个参数后返回一个等待第二个参数的函数。例如,λ(a:A). λ(b:B). a\tLam{a}{A}{\tLam{b}{B}{a}} 的类型是 A→B→AA \to B \to A,把它应用于 x:Ax : A 和 y:By : B 写作

(λ(a:A). λ(b:B). a)  x  y  =  ((λ(a:A). λ(b:B). a)  x)  y.(\tLam{a}{A}{\tLam{b}{B}{a}})\;x\;y \;=\; ((\tLam{a}{A}{\tLam{b}{B}{a}})\;x)\;y.

两条结合性约定是配套设计的:类型为 A→B→CA \to B \to C 的柯里化函数以 f  a  bf\;a\;b 的形式应用时,两边都不需要括号。

绑定与作用域

在 λ(x:A). t\tLam{x}{A}{t} 和 Π(x:A). B\tPi{x}{A}{B} 中,变量 xx 是约束的(bound),它的作用域是函数体 tt 或 BB。标注 AA 在作用域之外:在 λ(x:x). x\tLam{x}{x}{x} 中,标注里的 xx 指向项外部的某个东西,而函数体里的 xx 指向参数。不处于任何对应绑定子作用域内的变量出现称为自由的(free)。例如,在 λ(x:P). f  x  y\tLam{x}{P}{f\;x\;y} 中,变量 PP、ff 和 yy 是自由的,xx 是约束的。

关于约束变量有两个事实贯穿全书,两者都在第 11 章中形式化。

  1. 约束变量的名字没有意义:λ(x:A). x\tLam{x}{A}{x} 和 λ(y:A). y\tLam{y}{A}{y} 是同一个函数。仅在约束变量名上有差异的项称为 α 等价的,内核必须把它们视为相等。
  2. 用项替换变量(类型规则和计算都需要这一操作)时,绝不能意外地把自由变量带进同名绑定子之下。把这件事做对就叫避免捕获的替换。

这两个事实都容易陈述,也都在真实的证明检查器中造成过真实的错误,所以它们有专门的一章。

代码中

同一文件还提供了一些小的构造函数,使得写项时不必嵌套 Box::new 调用:var("x")、const_term("Nat")、app(f, a)、pi("x", a, b)、lam("x", a, b),以及构造 pi("_", a, b) 的 arrow(a, b)。三个辅助函数直接编码了本章的约定。

/// f a_1 ... a_n   (左结合的应用)
pub fn apps(f: Term, args: impl IntoIterator<Item = Term>) -> Term {
    args.into_iter().fold(f, app)
}

/// Pi (x_1 : A_1) ... Pi (x_k : A_k). tail   (由绑定子组成的 telescope)
pub fn telescope(binders: &[(String, Term)], tail: Term) -> Term {
    binders.iter().rev().fold(tail, |body, (name, ty)| pi(name, ty.clone(), body))
}

/// 把 t 分解为它的脊(spine):一个不是应用的头部,以及它的参数。
/// f a b  变成  (f, [a, b]);非应用的项变成  (t, [])。
pub fn spine(t: &Term) -> (Term, Vec<Term>) {
    match t {
        Term::App(f, a) => {
            let (head, mut args) = spine(f);
            args.push((**a).clone());
            (head, args)
        }
        _ => (t.clone(), vec![]),
    }
}

apps 和 spine 互为逆操作:apps(h, args) 构造左嵌套的应用 h  a1⋯anh\;a_1 \cdots a_n,spine 再把它拆开。脊(spine)视角是第 12 章的归约引擎看待项的方式,所以现在就熟悉它。嵌套项 ((f  a)  b)  c((f\;a)\;b)\;c 的头部是 ff,参数是 [a,b,c][a, b, c];沿每个应用的函数一侧向下走就到达头部,沿途收集的参数由内向外读,顺序正好。

本书通篇使用的 .leanmini 脚本的表层语法把同样的项写成 Sort 0、forall (x : A), B、A -> B、fun (x : A) => b,应用则用并置表示。附录 E给出完整文法。

练习

把命题“对所有命题 PP 和 QQ,PP 蕴含 QQ 蕴含 PP”写成一个项,并给出它的证明项。

解答

该命题是 Π(P:Sort 0). Π(Q:Sort 0). P→Q→P\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{P \to Q \to P}},展开后为 Π(P:Sort 0). Π(Q:Sort 0). Π(_:P). Π(_:Q). P\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{\tPi{\_}{P}{\tPi{\_}{Q}{P}}}}。 一个证明是 λ(P:Sort 0). λ(Q:Sort 0). λ(p:P). λ(q:Q). p\tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{p}{P}{\tLam{q}{Q}{p}}}}。用 leanmini 语法写出:

goal forall (P : Sort 0), forall (Q : Sort 0), P -> Q -> P
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (p : P) => fun (q : Q) => p
qed

给 f  a  (g  b)  cf\;a\;(g\;b)\;c 和 A→(B→C)→DA \to (B \to C) \to D 加上全部括号,然后给出第一个项的脊。

解答

f  a  (g  b)  c=((f  a)  (g  b))  cf\;a\;(g\;b)\;c = ((f\;a)\;(g\;b))\;c,头部为 ff,参数为 [a,  g  b,  c][a,\; g\;b,\; c]。注意由于括号的存在,g  bg\;b 仍是单个参数。 A→(B→C)→D=A→((B→C)→D)A \to (B \to C) \to D = A \to ((B \to C) \to D),即 Π(_:A). Π(_:(Π(_:B). C)). D\tPi{\_}{A}{\tPi{\_}{(\tPi{\_}{B}{C})}{D}}。

在 λ(x:A). λ(y:x). λ(x:Sort 0). y\tLam{x}{A}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}} 中,各处出现的 xx 分别指向哪个绑定子?哪些变量是自由的?

解答

yy 的标注中的 xx 指向外层绑定子 λ(x:A)\lambda(x : A),因为此时还没有进入内层的 λ(x:Sort 0)\lambda(x : \Sort{0})。最内层的 xx 由内层绑定子绑定但从未被使用,而唯一的自由变量是 AA。这个项会在第 11 章再次出现,在那里它让一个处理遮蔽不够小心的类型检查器出错。

要点回顾

  • 项有宇宙 Sort u\Sort{u}、变量、Π\Pi 类型、λ\lambda 抽象和应用五种,类型和项共用一套文法。
  • A→BA \to B 是函数体不使用其变量的 Π\Pi 类型的缩写。应用向左结合,-> 向右结合,函数是柯里化的。
  • 约束变量的名字可以任取,替换必须避免捕获,两者都在第 11 章处理。
  • 在 Rust 中,Term 是每种形式对应一个变体的枚举,spine/apps 在嵌套应用与“头部加参数列表”之间来回转换。