substitution 设计
设计目标
代换是库中其他所有部分赖以构建的操作:β 归约把实参代换给形参,重写规则实例化其变量,下游包中的部分求值用值替换已知变量。substitution 为 Term[T] 提供一种避免捕获的同时代换,并为任意 BindingSyntax AST 提供同一算法,其定律在 α-等价意义下成立。
数学背景
项、FV、names 和 =α 均沿用 syntax 设计 中的定义。
代换
代换是从名字到项的映射 σ,在有限定义域 domσ 之外为恒等 σ(x)=x。它的支撑是 suppσ=domσ∪⋃x∈domσnames(σ(x)),σ∖x 表示从定义域中移除 x。对名字集合 S,σ 在 S 上的相关值域为
Rσ(S)=z∈S∩domσ⋃FV(σ(z)).
避免捕获的同时代换
Substitution::apply 按如下方式计算 tσ:
vσ(βx.t)σ=v,xσ=σ(x),t(u1,…,un)σ=(tσ)(u1σ,…,unσ),={βx.tσ′βx′.(t{x↦x′})σ′if x∈/Rσ′(FV(t)),otherwise,σ′=σ∖x,
其中 x′=fresh(x, suppσ′∪names(t)∪{x}),t{x↦x′} 是 syntax 设计中带检查的约束变量重命名。情形划分是精确的:只有当实际插入 t 的某个替换项中 x 自由出现时,才重命名绑定子。
设计决策
同时、一遍完成
问题。 把 {x↦y, y↦2} 作用于 x+y,应该得到 y+2 还是 2+2?
备选方案。 顺序代换(逐条依次应用)、迭代代换(重复直到不再变化),或同时代换(每个变量只在原始项中查找一次)。
选择。 同时代换。变量情形 xσ=σ(x) 只查找 x 一次,且不会再访问插入的项:
(x+y){x↦y, y↦2}=x{…}+y{…}=y+2.
同时代换是具有代数结构的那一种(见下文的复合),它总会终止,并能直接表达诸如 {x↦y, y↦x} 的交换。顺序应用仍可通过 then 复合来实现,而迭代到不动点则是调用方的策略。泛型版本命名为 apply_once,以明确这一点。
仅在必要时重命名绑定子
问题。 当绑定子 βx 位于某个插入的替换项之上、且该替换项中 x 自由出现时,就会发生捕获。重命名所有绑定子可以避免捕获,但会让结果难以阅读。
选择。 仅当 x∈Rσ′(FV(t)) 时才重命名,并以 x 为提示选择新鲜名字。新鲜名字必须避开三个集合,各有其理由:
- 相关替换项的 FV,否则被重命名的绑定子会再次捕获它们;
- domσ′,否则从 x 重命名为 x′ 的那些出现本身会被代换(回归测试 “fresh binders avoid the substitution domain”);
- names(t),这样带检查的约束变量重命名 t{x↦x′} 不会失败,被重命名的变量也不会与同名的内层绑定子混淆。
suppσ′ 涵盖了前两个集合。因此实现中的 abort 不可达:由新鲜性引理 x′∈/names(t),这恰好是 alpha_rename_bound 的附加条件。
复合作为一等操作
then 构造 σ;τ,其中
(σ;τ)(x)=⎩⎨⎧σ(x)ττ(x)xx∈domσ,x∈domτ∖domσ,otherwise,
这样顺序应用就可以表示为一次同时代换,它开销更小(一次遍历),并满足下文的定律。
正确性 / 不变量
自由变量
引理 1。 FV(tσ)=⋃z∈FV(t)FV(σ(z))。
对 t 归纳。变量、值和应用情形是直接的。对于不需重命名的 βx.t,令 σ′=σ∖x,并假设 x∈/Rσ′(FV(t)):
FV((βx.t)σ)=FV(tσ′)∖{x}=(⋃z∈FV(t)FV(σ′(z)))∖{x}=⋃z∈FV(t),z=xFV(σ(z))=⋃z∈FV(βx.t)FV(σ(z)).induction(∗)
步骤 (∗):对 z=x,σ′(x)=x 贡献 {x},而它被移除了。对 z=x,σ′(z)=σ(z),且 x∈/FV(σ(z)):要么 z∈domσ′ 且 FV(σ(z))⊆Rσ′(FV(t))∋x,要么 σ(z)=z=x。有重命名时,同样的计算适用于 x′ 和 t{x↦x′},其中 x′∈/suppσ′ 保证了附加条件成立。□
推论(无捕获)。 在插入的替换项 σ(z)(z∈FV(t))中自由的变量,在 tσ 中也是自由的。
只有自由变量起作用
引理 2。 tσ=αt(σ∣FV(t)),其中 σ∣S 即 restrict(S)。
变量情形即定义本身。在绑定子情形,重命名测试已经限制在 Rσ′(FV(t)) 上,因此两边重命名的是相同的绑定子,并且只查找 FV(t)∖{x} 中的变量。新鲜名字可能不同,因为它们避开的是不同代换的支撑,所以相等只在 =α 意义下成立。
α-不变性
引理 3。 若 t=αt′,则 tσ=αt′σ。
只需检查一步 α 变换 βx.t=αβy.t{x↦y},其中 y∈/names(t)。两边都成为在同一个体(至多相差约束名)上的绑定子,并由引理 1,它们的体在绑定子之外具有相同的自由变量,因此它们 α-等价。由此,代换在 α-等价类上是良定义的,新鲜名字的选择在语义上无关紧要。
复合
引理 4。 t(σ;τ)=α(tσ)τ。
由引理 3,可以选取 t 的一个代表元,使其中没有绑定子落在 suppσ∪suppτ 中;这样两边都不会重命名任何绑定子,两个代换都原样穿过绑定子。变量情形按 σ;τ 的定义分情况:
x∈domσ:x∈domτ∖domσ:otherwise:x(σ;τ)=σ(x)τ=(xσ)τ,x(σ;τ)=τ(x)=xτ=(xσ)τ,x(σ;τ)=x=(xσ)τ.
应用和值的情形由归纳得出。□
推论(代换引理)。 对 x=y 且 x∈/FV(r),
t[x:=s][y:=r]=αt[y:=r][x:=s[y:=r]].
由引理 4,两边都是单个同时代换。左边是 {x↦s[y:=r], y↦r}。右边是 {y↦r[x:=s[y:=r]], x↦s[y:=r]},并且由于 x∈/FV(r)(引理 1),有 r[x:=…]=r。两个映射相等,因此结果 α-等价。11 Barendregt, The Lambda Calculus, 引理 2.1.16。 正是这一引理使 lambda 演算各包中的 β 归约与代换相容。
重命名嵌入代换
对重命名 ρ 和名字列表 L⊇FV(t),from_renaming(L, ρ) 为 σρ={x↦ρ(x)∣x∈L, ρ(x)=x}(作为变量),且 tσρ=αtρ:两者都把每个自由的 x 替换为变量 ρ(x),并且都恰好在会发生捕获时为绑定子取新鲜名字。
开销
每个绑定子都要计算其体的自由变量,因此对大小为 n、绑定深度为 d 的项,apply 的开销是 O(n⋅d) 次哈希集合操作,外加每个被重命名的绑定子额外一次体遍历。then 的开销是 self 的每个条目一次 apply。
泛型代换
GenericSubstitution::apply_once 是同一定义,只是每个构造子都换成了对应的 BindingSyntax 版本,因此引理 1–3 对任何满足 adapter 设计 中视图定律的实现都成立。在 Term[T] 上两种算法一致。
被否决的替代方案
- Barendregt 变量约定。 假定约束名与所有自由名都不同,可以去掉重命名情形;但项来自用户和其他算法,而且该约定在归约下不保持。因此本库改为显式重命名。
- 仅在 De Bruijn 项上做代换。 基于索引的代换不需要重命名,由 debruijn 提供;但下游 AST 共享的接口是具名的,因此具名代换本身必须正确。
- 迭代代换。 重复直到定义域变量不再出现可能发散(x↦f(x)),且没有复合律。需要不动点的调用方应显式迭代。
边界
- 没有合一、匹配或出现检查(occurs check):代换是给定的,而不是求解得到的。
GenericSubstitution 没有 then 或 restrict;要复合泛型代换,请依次应用它们。
- 结果只在 =α 意义下等于教科书定义;请用
@syntax.alpha_equal 比较它们。
- 值从不会被进入,因此包含变量的
Value 载荷不会被代换。