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

第 11 章

替换、捕获与 α 等价

前几章把项上的三种操作当作黑盒:替换(substitution),T-App、Freshen 和每一步计算都要用它;弱头规范化(weak head normalization),用来把 Π\Pi 或宇宙暴露出来;定义相等(definitional equality),Check 要用它。这三种操作都属于可信内核,而内核的多数 bug 就藏在其中,因为它们必须正确处理约束变量的名字。本章打开第一个黑盒,即替换,接着展示同样的命名问题如何出现在上下文里,最后定义两个项何时在约束变量的名字意义下相等。

自由变量与替换

Π\Pi 和 λ\lambda 会绑定它们的变量:在 λ(x:A). t\tLam{x}{A}{t} 中,tt 内部出现的 xx 指的是参数。绑定子的作用域只有函数体;xx 在标注 AA 中并不受绑定。一个变量出现若不被任何外层的 Π\Pi 或 λ\lambda 绑定,就是自由的。

定义 11.1(自由变量)。

FV(Sort u)=∅FV(Π(x:A). B)=FV(A)∪(FV(B)∖{x})FV(x)={x}FV(λ(x:A). t)=FV(A)∪(FV(t)∖{x})FV(t  s)=FV(t)∪FV(s)FV(c)=∅\begin{array}{rclcrcl} \FV(\Sort{u}) &=& \emptyset &\qquad& \FV(\tPi{x}{A}{B}) &=& \FV(A) \cup (\FV(B) \setminus \{x\}) \\[2pt] \FV(x) &=& \{x\} && \FV(\tLam{x}{A}{t}) &=& \FV(A) \cup (\FV(t) \setminus \{x\}) \\[2pt] \FV(t\;s) &=& \FV(t) \cup \FV(s) && \FV(c) &=& \emptyset \end{array}

例如,FV(λ(x:P). f  x  y)={P,f,y}\FV(\tLam{x}{P}{f\;x\;y}) = \{P, f, y\},而 FV(Π(P:Sort 0). P→P)=∅\FV(\tPi{P}{\Sort{0}}{P \to P}) = \emptyset,所以后者是一个封闭项。常量永远不会被绑定,因此不贡献任何自由变量。

下面的浏览器(explorer)把每个约束变量涂成与其绑定子相同的颜色,并给自由变量加下划线;把鼠标悬停在变量上可以看到它的绑定子。试试带遮蔽的项,例如 fun (x : A) => fun (x : A) => x。

我们用 t[x↦s]\subst{t}{x}{s} 表示把 tt 中自由出现的 xx 替换为 ss 的结果。遇到绑定子时需要格外小心:在 (λ(y:A). x)[x↦y]\subst{(\tLam{y}{A}{x})}{x}{y} 中,函数体的意思是“忽略参数,返回外层的 xx”。如果直接用 yy 替换 xx,就会得到恒等函数 λ(y:A). y\tLam{y}{A}{y},因为原本自由的 yy 被绑定子捕获了。为保持原意,替换前必须先给绑定子改名。

定义 11.2(避免捕获的替换)。 对 □∈{Π,λ}\square \in \{\Pi, \lambda\}:

x[x↦s]=sy[x↦s]=y(y≠x)Sort u[x↦s]=Sort uc[x↦s]=c(t1  t2)[x↦s]=t1[x↦s]    t2[x↦s](□(x:A). B)[x↦s]=□(x:A[x↦s]). B(x is shadowed)(□(y:A). B)[x↦s]=□(y:A[x↦s]). B[x↦s](y≠x,  y∉FV(s))(□(y:A). B)[x↦s]=□(z:A[x↦s]). (B[y↦z])[x↦s](y≠x,  y∈FV(s),  z fresh)\begin{array}{rcll} \subst{x}{x}{s} &=& s \\[2pt] \subst{y}{x}{s} &=& y & (y \neq x) \\[2pt] \subst{\Sort{u}}{x}{s} &=& \Sort{u} \\[2pt] \subst{c}{x}{s} &=& c \\[2pt] \subst{(t_1\;t_2)}{x}{s} &=& \subst{t_1}{x}{s}\;\;\subst{t_2}{x}{s} \\[2pt] \subst{(\square(x{:}A).\,B)}{x}{s} &=& \square(x{:}\subst{A}{x}{s}).\,B & \text{($x$ is shadowed)} \\[2pt] \subst{(\square(y{:}A).\,B)}{x}{s} &=& \square(y{:}\subst{A}{x}{s}).\,\subst{B}{x}{s} & (y \neq x,\; y \notin \FV(s)) \\[2pt] \subst{(\square(y{:}A).\,B)}{x}{s} &=& \square(z{:}\subst{A}{x}{s}).\,\subst{(\subst{B}{y}{z})}{x}{s} & (y \neq x,\; y \in \FV(s),\; z \text{ fresh}) \end{array}

这里“zz fresh”(新鲜)的意思是 z∉FV(s)∪FV(B)∪{x}z \notin \FV(s) \cup \FV(B) \cup \{x\}。

下面的例子逐一覆盖绑定子的各种情形。

替换 结果 原因
(Π(h:P). P)[P↦Q]\subst{(\tPi{h}{P}{P})}{P}{Q} Π(h:Q). Q\tPi{h}{Q}{Q} 不可能发生捕获;这是第 6 章的 T-App 步骤
(λ(x:A). x)[x↦s]\subst{(\tLam{x}{A}{x})}{x}{s} λ(x:A). x\tLam{x}{A}{x} xx 被遮蔽;函数体保持不动
(λ(y:x). y)[x↦s]\subst{(\tLam{y}{x}{y})}{x}{s} λ(y:s). y\tLam{y}{s}{y} 标注在 yy 的作用域之外
(λ(y:A). x  y)[x↦f  y]\subst{(\tLam{y}{A}{x\;y})}{x}{f\;y} λ(z:A). f  y  z\tLam{z}{A}{f\;y\;z} y∈FV(f  y)y \in \FV(f\;y),所以绑定子被改名为 zz

在最后一个例子里,替换项中的自由 yy 仍然指向外层的 yy,而约束变量(现在叫 zz)仍然指向参数。若不改名,结果会是 λ(y:A). f  y  y\tLam{y}{A}{f\;y\;y},它把参数传了两次。下面的浏览器并排执行这两种替换;颜色显示结果中的 yy 究竟是替换项里的自由变量,还是被绑定子捕获的那个。

上下文中的变量捕获

人们很容易想去掉 C-Ext 的附加条件 x∉dom(Γ)x \notin \dom(\Gamma),让新假设直接遮蔽旧假设,就像大多数编程语言那样,但这样做会使内核不可靠。考虑

t=λ(x:Sort 0). λ(y:x). λ(x:Sort 0). y.t = \tLam{x}{\Sort{0}}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}}.

变量 yy 的类型是外层的 xx,而最内层的 λ\lambda 忽略自己的参数。所以 tt 的正确类型是

Π(x:Sort 0). Π(y:x). Π(x′:Sort 0). x.\tPi{x}{\Sort{0}}{\tPi{y}{x}{\tPi{x'}{\Sort{0}}{x}}}.

一个没有 Freshen\Freshen 的检查器会在上下文 x:Sort 0,  y:x,  x:Sort 0x : \Sort{0},\; y : x,\; x : \Sort{0} 中查找 yy,并返回它的类型,也就是光秃秃的名字 xx。此时 xx 指的是内层绑定子,于是检查器返回

Π(x:Sort 0). Π(y:x). Π(x:Sort 0). x,\tPi{x}{\Sort{0}}{\tPi{y}{x}{\tPi{x}{\Sort{0}}{x}}},

它的意思是“从任意类型的任意元素出发,可以造出每一个类型的元素”。给定 A:Sort 0A : \Sort{0} 和 a:Aa : A,项 t  A  at\;A\;a 的类型就是 Π(B:Sort 0). B\tPi{B}{\Sort{0}}{B},即 False 的一个证明。而这样的 AA 和 aa 在空上下文中就已存在:A=Π(P:Sort 0). P→PA = \tPi{P}{\Sort{0}}{P \to P} 本身就是一个命题,恒等函数 a=λ(P:Sort 0). λ(h:P). ha = \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}} 证明了它。

有了 Freshen\Freshen,最内层的绑定子在进入上下文之前会被改名为 x′x',上下文因此变成 x:Sort 0,  y:x,  x′:Sort 0x : \Sort{0},\; y : x,\; x' : \Sort{0}。查找 yy 返回 xx,它仍然指向外层绑定子,于是 Infer\Infer 返回上面那个正确的类型。推导浏览器会展示这次改名:最内层 T-Lam 节点上的注记报告了它,叶子节点的上下文中也出现了 x′x'。

# 内核把内层的 x 改名为 x',并推断出正确的类型。
# 如果内核允许内层的 x 遮蔽外层的 x,它就会接受这个项
# 作为第二个陈述的证明,而第二个陈述是假的。
goal forall (x : Sort 0), forall (y : x), forall (x' : Sort 0), x
exact fun (x : Sort 0) => fun (y : x) => fun (x : Sort 0) => y
qed
goal forall (x : Sort 0), forall (y : x), forall (x' : Sort 0), x'
exact fun (x : Sort 0) => fun (y : x) => fun (x : Sort 0) => y
qed

这个问题和替换中的问题是同一个:一个名字被移进了另一个作用域,而它在那里的含义变了。任何按名字识别变量的表示方式都会受影响,无论上下文是列表还是哈希表。常见的补救办法有三种。

  1. 直接拒绝遮蔽,这是最简单的做法,但 A→BA \to B 使用的保留名 _ 必须例外,且永远不加入上下文。
  2. 进入时改名,也就是 Freshen\Freshen 的做法,这是我们的选择。
  3. 用位置表示约束变量(de Bruijn 索引 [de Bruijn 1972]),或采用局部无名(locally nameless)风格:约束变量用索引,自由变量带唯一标识符;Lean 4 采用后一种方式。

测试套件应当包含上面的 tt,并检查内核要么拒绝它,要么给它赋予正确的类型。

α 等价

约束变量的名字不携带任何含义:λ(x:A). x\tLam{x}{A}{x} 和 λ(y:A). y\tLam{y}{A}{y} 是同一个函数,Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} 和 Π(Q:Sort 0). Q→Q\tPi{Q}{\Sort{0}}{Q \to Q} 是同一个命题。只在约束变量名字上有差别的项称为 α 等价(alpha-equivalent)的。内核不能用单纯的语法相等来比较类型,否则第二对项就会被拒绝。

定义 11.3(α 等价)。 =α\alphaeq 是项上最小的合同关系,使得对 □∈{Π,λ}\square \in \{\Pi, \lambda\},

□(x:A). B  =α  □(y:A). B[x↦y]whenever y∉FV(B).\square(x{:}A).\,B \;\alphaeq\; \square(y{:}A).\,\subst{B}{x}{y} \qquad \text{whenever } y \notin \FV(B).

(合同关系(congruence)是指被每个项构造器保持的等价关系:各部分相关,则整体相关。)这个定义容易陈述,却不能直接执行,因为它没有说该选哪些名字。实现的做法是同步遍历两个项,并为每一侧记住到目前为止经过的绑定子列表。这样,约束变量就由它指向哪一个绑定子来识别,而不是由名字。

为了描述这个遍历过程,我们使用形如 E1;E2⊢s=αtE_1; E_2 \vdash s \alphaeq t 的判断,其中 E1E_1 和 E2E_2 是名字列表。E1E_1 记录包围 ss 的绑定子,从最外层到最内层;E2E_2 记录包围 tt 的绑定子。这个判断读作:“放在绑定子 E1E_1 之内的 ss 与放在绑定子 E2E_2 之内的 tt,在约束变量名字的意义下是同一个项”。之所以需要两个列表,是因为两个项对应的绑定子可能使用不同的名字。例如,比较 λ(x:A). λ(y:B). x\tLam{x}{A}{\tLam{y}{B}{x}} 与 λ(a:A). λ(b:B). a\tLam{a}{A}{\tLam{b}{B}{a}} 时,两侧各经过两个绑定子,到达

x,y;  a,b⊢x=αa,x, y;\; a, b \vdash x \alphaeq a,

它成立,因为 xx 和 aa 都指向两个绑定子中外层的那个。比较两个完整的项时从空列表出发:⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t。

为了把“同一个绑定子”说精确,令 idx(E,x)\mathrm{idx}(E, x) 为 xx 在 EE 中最后一次出现的位置,从右边开始、自 00 起计数;若 x∉Ex \notin E 则无定义。在上面的例子中,idx((x,y),x)=idx((a,b),a)=1\mathrm{idx}((x, y), x) = \mathrm{idx}((a, b), a) = 1。取最后一次出现是为了处理遮蔽:在 λ(x:A). λ(x:B). x\tLam{x}{A}{\tLam{x}{B}{x}} 中,函数体指的是内层的 xx。

图 11.1。算法化的 α 等价。由定理 11.4,两个项 ss 和 tt α 等价当且仅当 ⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t。

E1;E2⊢Sort u=αSort u;(α-Sort)E1;E2⊢c=αc;(α-Const)\dfrac{}{E_1; E_2 \vdash \Sort{u} \alphaeq \Sort{u}}\rulename{$\alpha$-Sort} \qquad \dfrac{}{E_1; E_2 \vdash c \alphaeq c}\rulename{$\alpha$-Const}
idx(E1,x)=idx(E2,y)=iE1;E2⊢x=αy;(α-Bound)x∉E1x∉E2E1;E2⊢x=αx;(α-Free)\dfrac{\mathrm{idx}(E_1, x) = \mathrm{idx}(E_2, y) = i}{E_1; E_2 \vdash x \alphaeq y}\rulename{$\alpha$-Bound} \qquad \dfrac{x \notin E_1 \qquad x \notin E_2}{E_1; E_2 \vdash x \alphaeq x}\rulename{$\alpha$-Free}
E1;E2⊢A=αA′E1,x;  E2,y⊢B=αB′E1;E2⊢Π(x:A). B=αΠ(y:A′). B′;(α-Pi)\dfrac{E_1; E_2 \vdash A \alphaeq A' \qquad E_1, x;\; E_2, y \vdash B \alphaeq B'}{E_1; E_2 \vdash \tPi{x}{A}{B} \alphaeq \tPi{y}{A'}{B'}}\rulename{$\alpha$-Pi}
E1;E2⊢A=αA′E1,x;  E2,y⊢t=αt′E1;E2⊢λ(x:A). t=αλ(y:A′). t′;(α-Lam)E1;E2⊢t1=αt1′E1;E2⊢t2=αt2′E1;E2⊢t1  t2=αt1′  t2′;(α-App)\dfrac{E_1; E_2 \vdash A \alphaeq A' \qquad E_1, x;\; E_2, y \vdash t \alphaeq t'}{E_1; E_2 \vdash \tLam{x}{A}{t} \alphaeq \tLam{y}{A'}{t'}}\rulename{$\alpha$-Lam} \qquad \dfrac{E_1; E_2 \vdash t_1 \alphaeq t_1' \qquad E_1; E_2 \vdash t_2 \alphaeq t_2'}{E_1; E_2 \vdash t_1\;t_2 \alphaeq t_1'\;t_2'}\rulename{$\alpha$-App}

自由变量按名字比较(α\alpha-Free),约束变量按位置比较(α\alpha-Bound),常量按名字比较(α\alpha-Const)。一侧受绑定、另一侧自由的变量不与任何东西相等,所以这种情形没有规则。由于两侧总是一起穿过绑定子,E1E_1 和 E2E_2 的长度始终相同。因此,从右数位置与从左数位置是一回事,实现可以把每个列表存成从名字到绑定深度的映射,只要离开绑定子时恢复先前的条目即可。如果处处都用位置代替名字,就得到 de Bruijn 表示:在这种表示下,α 等价的项在语法上完全相同。

示例

第 6 章的第一个例子最后比较了推断出的类型与声明的类型,其中 P→PP \to P 是 Π(_:P). P\tPi{\_}{P}{P} 的缩写:

  • ⋅ ;⋅⊢Π(P:Sort 0). Π(h:P). P=αΠ(P:Sort 0). Π(_:P). P\cdot\,;\cdot \vdash \tPi{P}{\Sort{0}}{\tPi{h}{P}{P}} \alphaeq \tPi{P}{\Sort{0}}{\tPi{\_}{P}{P}} α\alpha-Pi
    • ⋅ ;⋅⊢Sort 0=αSort 0\cdot\,;\cdot \vdash \Sort{0} \alphaeq \Sort{0}:成立 α\alpha-Sort
    • P; P⊢Π(h:P). P=αΠ(_:P). PP;\, P \vdash \tPi{h}{P}{P} \alphaeq \tPi{\_}{P}{P} α\alpha-Pi
      • P; P⊢P=αPP;\, P \vdash P \alphaeq P:成立,两侧的索引都是 00 α\alpha-Bound
      • P,h;  P,_⊢P=αPP, h;\; P, \_ \vdash P \alphaeq P:成立,两侧的索引都是 11 α\alpha-Bound

名字 hh 和 _\_ 不同,但两者都从未被查找,所以无关紧要。第 6 章的那个错误证明则在最后一步失败:

  • ⋅ ;⋅⊢Π(P:Sort 0). Π(h:P). Sort 0=αΠ(P:Sort 0). Π(_:P). P\cdot\,;\cdot \vdash \tPi{P}{\Sort{0}}{\tPi{h}{P}{\Sort{0}}} \alphaeq \tPi{P}{\Sort{0}}{\tPi{\_}{P}{P}} α\alpha-Pi
    • ⋅ ;⋅⊢Sort 0=αSort 0\cdot\,;\cdot \vdash \Sort{0} \alphaeq \Sort{0}:成立 α\alpha-Sort
    • P; P⊢Π(h:P). Sort 0=αΠ(_:P). PP;\, P \vdash \tPi{h}{P}{\Sort{0}} \alphaeq \tPi{\_}{P}{P} α\alpha-Pi
      • P; P⊢P=αPP;\, P \vdash P \alphaeq P:成立 α\alpha-Bound
      • P,h;  P,_⊢Sort 0=αPP, h;\; P, \_ \vdash \Sort{0} \alphaeq P:没有规则适用

位置还能区分那些粗心地按名字比较时会混淆的项。λ(x:A). λ(y:A). x\tLam{x}{A}{\tLam{y}{A}{x}} 和 λ(a:A). λ(b:A). b\tLam{a}{A}{\tLam{b}{A}{b}} 不是 α 等价的:两个函数体到达 x,y; a,b⊢x=αbx, y;\, a, b \vdash x \alphaeq b,而 idx\mathrm{idx} 给 xx 的值是 11,给 bb 的值却是 00。反过来,λ(x:A). λ(x:A). x\tLam{x}{A}{\tLam{x}{A}{x}} 和 λ(a:A). λ(b:A). b\tLam{a}{A}{\tLam{b}{A}{b}} 是 α 等价的,因为 xx 在 (x,x)(x, x) 中最后一次出现的索引是 00,与 bb 在 (a,b)(a, b) 中的索引相同。

图 11.1 的规则与定义 11.3 是两个不同的定义,所以我们必须检查它们一致。

定理 11.4(算法化 α 等价的正确性)。 对所有项 ss 和 tt,s=αts \alphaeq t 在定义 11.3 的意义下成立,当且仅当 ⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t 可由图 11.1 的规则推导出来。

证明的思路是,两边说的是同一件事:ss 和 tt 有相同的 de Bruijn 表示。对算法而言这几乎是显然的,因为它的规则比较位置的方式正是 de Bruijn 索引的方式;对定义 11.3 而言,关键事实是给约束变量改名不改变 de Bruijn 表示。附录 B 给出完整证明。

练习

逐步计算 (λ(y:A). λ(x:B). x  y)[x↦y]\subst{(\tLam{y}{A}{\tLam{x}{B}{x\;y}})}{x}{y}。

解答

外层绑定子是 yy,它在替换项 yy 中自由出现,所以适用捕获情形:把 yy 改名为新鲜的 zz,得到函数体 λ(x:B). x  z\tLam{x}{B}{x\;z},然后再替换。内层绑定子是 xx,正是被替换的变量,所以内层函数体保持不动。结果:λ(z:A). λ(x:B). x  z\tLam{z}{A}{\tLam{x}{B}{x\;z}}。注意内层的 xx 从来不是自由出现,所以实际上什么都没被替换;唯一的效果是避免捕获所强制的改名。

运行图 11.1 的规则,判断 λ(x:A). λ(y:A). y  x\tLam{x}{A}{\tLam{y}{A}{y\;x}} 与 λ(y:A). λ(x:A). x  y\tLam{y}{A}{\tLam{x}{A}{x\;y}} 是否 α 等价。

解答

是。经过两步 α\alpha-Lam,绑定子列表分别为 x,yx, y 和 y,xy, x,函数体则为 y  xy\;x 和 x  yx\;y。α\alpha-App 先比较 yy 与 xx,得到的索引分别是 00 和 00;再比较 xx 与 yy,索引分别是 11 和 11。名字虽然互换,位置却一致。

要点回顾

  • 替换只替换自由出现;当替换项提到绑定子的名字时(捕获),必须给绑定子改名。
  • 同样的捕获问题也出现在上下文中。允许绑定子遮蔽上下文条目的内核可以证明 False;Freshen 防止了这一点。
  • α 等价按绑定子的位置比较约束变量,按名字比较自由变量。图 11.1 是算法;定理 11.4 说明它与定义一致。
  • 这三者都在 src/substitution.rs 中:subst、fv 和 alpha。