Skip to content

算法Algorithm

Algorithm W

施工中 · 内容逐步修订

Algorithm W

完整传播HM替换、区分环境共享与let泛化,并通过嵌套推断账本和MGU解集不变量构造主类型。

形式陈述 ​

输入、替换与最一般性 ​

固定纯Hindley–Milner语言,项由变量、整数和布尔常量、λ、应用、非递归 let 与二元组构成。单态类型和方案为

τ::=α∣Int∣Bool∣τ→τ∣τ×τ,σ::=∀α¯.τ.

“单态”在本页指没有量词,仍允许自由类型变量。环境 Γ 把项变量映到方案;λ 参数只绑定量词为空的方案。替换 S 是类型变量到有限类型树的有限映射,一次应用是同时替换:若 S(α)=τ,此次返回该 τ,不继续用同一个 S 展开其内部变量。复合另行定义为

(S2∘S1)(τ)=S2(S1τ).

对方案,只替换自由变量,并先把量化变量改名到对替换像新鲜的位置。例如 S={α↦β} 时,

S(∀β.α→β)=∀γ.β→γ,

右侧的 β 仍自由。把它写成 ∀β.β→β 就发生了捕获。SΓ 逐个作用于环境方案。[1]

W(Γ,e) 成功时返回 (S,τ),满足

SΓ⊢e:τ.

它返回的还不只是某个可用类型。在类型变量统一改名后,若某替换 R 使 RΓ⊢e:τ′,则存在进一步替换 T,使

Rα=(T∘S)α(α∈FTV(Γ)),τ′=Tτ.

这表达环境约束和结果类型的联合最一般性,等式只要求在原环境的自由变量上比较两替换。特别地,若 FTV(Γ)=∅,则 gen(Γ,τ) 是主类型方案,其他可得单态类型都是它的实例。标准纯 HM 的可靠性与完备性支持这些结论;加入状态、高秩或多态递归后需重新论证。[1]

全部递归分支 ​

fresh 每次返回从未使用过的类型变量;inst 把方案的每个量化变量换成相互不同的新鲜变量,保留其自由变量。unify 返回最一般统一子,失败则整个 W 失败。约定 Γ[x↦σ] 遮蔽旧的 x;项绑定器也可预先统一改名。算法为

text
W(Γ, integer) = (Id, Int)
W(Γ, boolean) = (Id, Bool)
W(Γ, x)       = (Id, inst(Γ(x)))       // fail if x is unbound

W(Γ, λx.e):
    α := fresh()
    (S, τ) := W(Γ[x ↦ α], e)
    return (S, Sα → τ)

W(Γ, e1 e2):
    (S1, τ1) := W(Γ, e1)
    (S2, τ2) := W(S1Γ, e2)
    β := fresh()
    U := unify({ S2τ1 = τ2 → β })
    return (U ◦ S2 ◦ S1, Uβ)

W(Γ, (e1,e2)):
    (S1, τ1) := W(Γ, e1)
    (S2, τ2) := W(S1Γ, e2)
    return (S2 ◦ S1, (S2τ1) × τ2)

W(Γ, let x=e1 in e2):
    (S1, τ1) := W(Γ, e1)
    Γ1 := S1Γ
    σ := ∀(FTV(τ1) \ FTV(Γ1)). τ1
    (S2, τ2) := W(Γ1[x ↦ σ], e2)
    return (S2 ◦ S1, τ2)

返回的 τ 已包含该次推断所得约束,不能把它当作仍待求解的原始占位符。λ 分支尤其要返回 Sα→τ;二元组第二分量可能继续约束第一分量,所以结果是 (S2τ1)×τ2。应用先改环境,再改先前函数类型;let 泛化的环境是 S1Γ。[1][2]

直觉

W 沿语法树一边走一边积累“到目前为止必须成立的类型等式”。每遇到未知值就生成新鲜类型变量,每遇到应用就让统一算法校准插头与插座,所得替换立即改写后续世界。let 是唯一的多态关口:先把局部求出的类型中不受环境约束的变量封装成方案,之后每次使用再新鲜实例化。它不是试遍所有类型,而是始终维护最一般解,因此一次运行就得到整个类型族。

Algorithm W 的泛化与实例化
例子与边界

对 λf. λx. f x,W 为 f,x 分配新鲜类型 α,β,应用处统一 α 与 β→γ,最终得到 (β→γ)→β→γ。对

let id = λx.x in (id 0, id true),

绑定处得到 α→α 并泛化为 ∀α.α→α,两次使用分别新鲜实例化,因此可同时接受整数和布尔值。

边界是省略 occurs check:推断 λx. x x 时会产生 α=α→β,标准有限类型下应失败。W 也不推断任意高秩或多态递归类型;给它加入语言特性后,主类型和完备性可能不再成立。

复合不是合并映射 ​

令 S1={α↦β}、S2={β↦Int}。正确复合把 α,β 都送到 Int;简单字典并集却在一次同时替换下仍把 α 送到 β。计算 S2∘S1 时应先用 S2 改写 S1 的每个像,再为不在 dom(S1) 的变量补入 S2 的映射。这也覆盖两个定义域相交的情形,因为原变量首先经过 S1。

嵌套 let 的完整替换账本 ​

考虑闭项

text
λf.
  let id = λx.x in
  let y = f (id 0) in
  let keep = λz.y in
  (id true, keep (id 0))

令 f 的初始单态类型为 α。id 的参数类型取新鲜 a,故得到 a→a;a 不自由出现于环境 f:α,于是方案为 ∀a.a→a。下表按实际从左到右的推断次序,记录所有应用处新增的统一约束与替换。每行替换都左复合到此前累积替换,并作用于后续环境。

位置 新鲜实例或结果变量 统一方程 本轮 MGU
第一次 id 0 id:b→b,结果 c b→b=Int→c b↦Int,c↦Int
f (id 0) 结果 d α=Int→d α↦Int→d
id true id:g→g,结果 h g→g=Bool→h g↦Bool,h↦Bool
第二次 id 0 id:j→j,结果 k j→j=Int→k j↦Int,k↦Int
keep (id 0) keep:i→d,结果 ℓ i→d=Int→ℓ i↦Int,d↦ℓ

两处 let 的处理发生在表的第二行之后,不能跳过:

  1. 绑定 y 时,环境已含 f:Int→d,所以 d∈FTV(Γ1)。y 的类型是 d,没有可泛化变量,方案仍是单态 d。
  2. 推断 λz.y 时,为 z 取新鲜 e,得到 e→d。此时环境含自由 d,但不含 e,故 keep 的方案为 ∀e.e→d。表末调用把量化的 e 新鲜化为 i,并保留共享的 d。

最后一行的 d↦ℓ 必须继续更新 f、y 和 keep 的自由部分。最终累积替换对所有已引入变量的非恒等作用为

S={b,c,j,k,i↦Int;g,h↦Bool;α↦Int→ℓ;d↦ℓ}.

这里 a,e 在各自方案中被量化,不是后续统一可以全局改写的共享槽位。二元组类型为 Bool×ℓ;最外层 λ 要用更新后的 Sα,因此闭项的主方案为

∀ℓ. (Int→ℓ)→(Bool×ℓ).

取 ℓ=Int 或 ℓ=Bool 都得到合法实例。若让 keep 泛化共享的 d,便错误允许它把同一个 y 返回为任意类型;若忘记更新 λ 参数,则结果还会保留不受约束的 α,同样不可靠。

统一器维护的是全部解的因子分解 ​

对原方程集合 E0,统一器维护残余集合 E 和累积替换 S,初态为 (E0,Id)。用 Sol(E) 表示所有满足 E 的有限类型替换,核心不变量是

Sol(E0)={R∘S∣R∈Sol(E)}.

每次取一条当前方程,按以下顺序处理:相同类型删去;同一箭头或积构造器分解成两个分量方程;若一侧为变量 α,把它朝向左侧,对另一侧 τ 做 occurs check,再令 T={α↦τ},把状态改为 (TErest,T∘S)。其余构造器冲突则失败。变量左右互换在这一分支中一次完成,不另作可来回翻转的循环步骤。[3]

删除、分解和朝向不改变解集。变量消去的关键等式是

Sol({α=τ}∪Erest)={R∘T∣R∈Sol(TErest)}.

从右往左:α 不在 τ 中,故 Tτ=τ,于是 R∘T 让两侧都成为 Rτ;残余方程也由假设满足。从左往右:若 Q 满足原方程,则 Qα=Qτ,从而 Q∘T=Q;它也满足 TErest,取 R=Q 即可。这证明新约束没有删除任何可进一步实例化的解。

终止时 E=∅,恒等替换满足它,所以累积 S 本身统一 E0;不变量还说明所有其他统一子都在 S 后继续替换,故 S 是 MGU。遇到不同构造器时,替换无法改变外层符号,解集为空。若 α 真出现在 τ≠α 内,任何有限解都会让 Qα 成为 Qτ 的真子树,二者不可能相同;这证明 occurs-check 拒绝可靠。应先删 α=α,不能把这条恒等式误判成递归类型。

算法终止也有单独度量:先比较残余方程中不同变量的个数,再比较全部类型树节点数。消去变量严格减少第一项,即使替换展开使第二项增加也无妨;删除和构造器分解在第一项不减时严格减少第二项。两项均为自然数,按字典序不可能无限下降。W 的递归调用又只处理原表达式的真子式,每次统一终止,故整个推断过程终止。

两种失败:循环类型与共享约束 ​

对 λx. x x,x 只有一个单态类型 α。应用要求 α=α→β,occurs check 可靠失败。循环类型图可能表示 rational tree,但那已改变本页有限 HM 类型的对象,不能用它偷偷接受该项。

再看

text
λf. let g = f in (g 0, g true)

f 的 α 已自由出现于环境,所以 g 不能得到 ∀α.α,只能保持同一个单态 α。第一次应用把它统一为 Int→β;第二次应用要求该同一函数接受 Bool,分解得到 Int=Bool,因此失败。let 不会仅因换了一个变量名就创造多态。

推论与应用

W 把类型方案、一阶统一和HM 推断组合成可实现算法。编译器可据此补全省略的类型、生成错误信息,并在中间表示中插入显式类型信息。

实际实现常用约束生成与求解分离、union–find、等级或区域标记来高效实现泛化;但这些优化必须保持 W 的主类型语义。

成功判断的证明按项结构归纳:变量取方案实例;常量直接定型;λ 用更新后的参数类型;应用对两个归纳结果施加后续替换,再由统一等式接上函数消去;二元组逐分量组合。let 则先在更新环境中泛化不属于环境的变量,再检查其作用域。最一般性还需在这项归纳中同时维护环境因子分解及新鲜实例化义务;MGU不变量解释统一步骤为何不作多余承诺,完整HM主类型定理见文献。[1] 不能只证明统一器输出一个解,就把let的泛化正确性当作已经证明。

参考资料
  • [1] Luis Damas and Robin Milner, Principal Type-Schemes for Functional Programs, POPL 1982, §§3、6–7;可访问重录本:方案替换、W及主类型定理。重录本保留了原应用/let公式的排印错误,本页规则由下面原文交叉核对,未照抄错误下标。
  • [2] Robin Milner, A Theory of Type Polymorphism in Programming, 1978, §4.1,pp. 365–366:替换顺序与统一子因子分解。该文证明可靠性,完备性结论采用1982年文献。
  • [3] Cornell CS4110, Lecture 23: Type Inference and Unification, 2016, §§2.2–2.2.2:同时替换、复合、occurs check和MGU规格。
关系图谱5 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系