Skip to content

定义Definition

值限制与安全泛化

Value restriction · Relaxed value restriction

限制带引用的let泛化,阻止同一存储单元被独立实例化为不兼容类型。

形式陈述 ​

值限制控制带副作用语言中let绑定的泛化。纯Hindley–Milner可以把不在环境中自由出现的类型变量量化;加入可变引用后,这条规则不能不加条件地套在每个右侧表达式上。

固定按值调用的ML核心,含整数、布尔、不可变列表及引用。ref e分配新单元,!r读取,r:=v写入。本页的基础版本只对句法上非扩张的右侧泛化:常量、变量、λ抽象,以及仅由非扩张成分组成的不可变构造值。函数应用、引用分配和赋值不在其中。具体语言对构造器、惰性项等还有细则,不能只按“求值最终返回一个值”判断。

若已推得 Γ⊢e:τ,记 A=FV(τ)∖FV(Γ)。let绑定采用

Genv(Γ,e,τ)={∀A.τ,e非扩张,τ,e扩张.

第二行保留未量化的共享类型变量。它们以后可被具体使用约束成Int等类型,但不能每次出现时重新实例化。实现常把这类尚未确定的单态变量显示为weak type variables。

直觉

一个多态恒等函数的不同使用可以选择不同类型,因为函数不保存一份会被后来的使用改写的“当前实参”。一个引用则是真实共享的存储单元:第一次把整数写进去,第二次仍读同一块内容。若两次使用可以各自任意选择该单元的元素类型,就会给一块内存同时签发互相冲突的读写许可。

关键不是“值里绝对不能含引用”。函数值可以捕获引用,但捕获环境中已有的类型变量不会被泛化;函数体内部也可以分配引用,只要每次调用的分配与类型实例相匹配。值限制与“只量化环境外的变量”要一起使用。

例子与边界

一次不安全泛化怎样真的卡住 ​

暂时错误地允许

text
let r = ref [] in
r := [8];
if head(!r) then 0 else 1

在分配处把r泛化成 ∀a.Ref(List(a))。写入时选择 a=Int,读取时另选 a=Bool,坏规则便会让整个程序看起来类型正确。

但运行只有一个地址ℓ:

步骤 运行时存储 错误静态视图
ref [] σ(ℓ)=[],r保存ℓ r被授予任意列表引用类型
r := [8] σ(ℓ)=[8] 当作Ref(List Int)写入
!r 读出同一个[8] 当作List Bool读取
head(!r) 得到整数8 被错误地认为是Bool
if 8 then ... 不是值,也没有布尔分支规则可用 程序卡住

此例没有空列表head错误,因为真正读出的列表非空。失败恰在整数被冒充布尔值,破坏进展与保持希望保证的运行时安全。

正确规则在 ref [] 处保留一个共享α,令 r:Ref(List(α))。写入[8]把α确定为Int;随后if要求同一个α为Bool,检查器报告Int与Bool冲突,程序在执行前被拒绝。

把共享单元改成工厂 ​

下面的接口安全:

text
let fresh = fun () -> ref [] in
let ri = fresh() in
let rb = fresh() in
ri := [8];
rb := [true]

fresh的右侧是λ值,可取得类型方案

∀a.Unit→Ref(List(a)).

两次调用各自产生新地址 ℓi≠ℓb。ri和rb仍分别是单态引用,但它们不共享同一个类型变量,因此可以一处存整数、一处存布尔。改变接口的代价也要看清:原来请求“一份共享缓存”,现在提供“每次新建一个缓存”,两种程序的状态共享语义不同。

反过来,若外层已有 r:Ref(List(α)),再写 let read = fun () -> !r,α仍自由出现在Γ中。即使右侧是λ,read也不能取得 ∀a.Unit→List(a);它只能读r固定的元素类型。

安全程序也可能被保守限制 ​

let id2 = (fun f -> f) (fun x -> x)没有副作用,结果确实是恒等函数。基础值限制仍把右侧看作应用,不泛化其 α→α。编译器不必为了每个let先证明任意函数调用都无状态;句法规则以少量误拒换取简单、局部的检查。

可以把它改成 fun x -> ((fun f -> f) (fun y -> y)) x,使外层成为λ值。不过一般程序的η展开可能把副作用从定义时移到调用时,甚至改变执行次数;不能把它当作无条件保持语义的修复按钮。

推论与应用

放宽的值限制允许对扩张表达式结果类型中只出现在协变位置的候选变量泛化。直观上,不可变List只把元素交给使用者,不允许使用者写入一个不同类型的元素,所以 (fun () -> [])()的结果可获得 ∀a.List(a)。

协变性与子类型中的方向相同:若A可替代B,List A可替代List B。函数域反向、函数余域同向,因此 a→a 中a同时出现在两种方向,不能靠这个放宽规则泛化。可读可写Ref必须不变,否则扩大可写类型后会把不允许的值写回窄类型视图;Ref(List a)中的a也不能据此放宽。

对抽象类型构造器,客户端还须知道接口声明的variance。实现私下使用不可变列表,而接口没有暴露协变性,并不让客户端自动获得这个证明。

值限制解决的是“何时安全地产生类型方案”。它不处理多态函数参数的量词位置,也不允许递归体自动在不同类型上调用自己;这些分别进入高秩多态与多态递归。

参考资料
  • Andrew M. Pitts, Lecture Notes on Types, 2015,§3:多态引用与安全泛化
  • OCaml manual, “Polymorphism and its limitations”, §§5.1.1–5.1.5:weak variables、值限制、放宽规则与variance
  • Andrew K. Wright, “Simple Imperative Polymorphism,” Lisp and Symbolic Computation 8, 1995, 343–355
关系图谱14 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

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