“多态递归让递归体内的自调用在不同元素类型上实例化,例如从Perfect a递归到Perfect(a×a)。高秩参数则把全称量词保留在函数箭头左侧,使同一个参数能在一次调用里处理Int与Boo…”
形式陈述
高秩多态允许函数参数本身具有全称多态类型。重点是量词相对函数箭头的位置,而不只是一个类型里写了几个∀。例如
要求调用者交来一个能在所有a上工作的函数。相反,
让调用者先选择某个a,然后只交来那个类型上的单态函数。两者都使用参数多态,但承诺的范围不同。
本页固定predicative的、由注解引导的高秩核心。单态类型τ不含∀;一般类型σ可把全称量词放到箭头左侧。实例化
检查采用双向检查的两个方向:
- 使用一个多态值:把它的量词实例化成新鲜、可求解的单态变量,再由实际参数约束
- 证明一个表达式具有全称类型:用新鲜刚性变量b代表任意类型,检查量词体;b不能被指定成Int等具体类型,也不能逃入外层环境或结果中的旧未知量
刚性变量表示“必须对任意类型成立”,可解变量表示“这一次还没选定的类型”。把二者都交给无条件合一,会把全称承诺错误地缩成某个碰巧成功的实例。
直觉
一个接口说“给我一把能开任意盒子的钥匙”,与“你先选一个盒子,再给我开它的钥匙”不同。前者能在一次调用中用同一个参数处理Int和Bool;后者只保证参数适用于本次已经选定的那个类型。
普通let多态允许在环境中保存一份方案,但传统HM的λ参数通常是单态的。把已经多态的id传入一个普通λ参数,不会自动让该参数在函数体中变成多态;需要在接口上保留这个量词。
例子与边界
一个参数,在同一次调用中用两种类型
定义
accept : (∀a. a → a) → Int × Bool
accept = λ(f : ∀a. a → a). (f 5, f true)
检查第一处f时,从方案得到新鲜α→α;实参5要求α=Int,因此第一项是Int。检查第二处f时,重新得到β→β;true要求β=Bool,因此第二项是Bool。α和β不是同一个未知量,最终二元组符合Int×Bool。
给定 id:∀a.a→a,accept id合法并得到 (5,true)。也可把 λx.x按期待的全称参数类型检查:引入刚性b,在x:b下有x:b,因此λx.x:b→b;b没有逃逸,量词检查成功。
若把accept签名换成 (5,true)就可实现它;只是它不足以支持当前函数体的两种调用。
刚性检查拒绝冒充通用函数
λx.x+1可以是Int→Int,却不能作为accept的参数。按
再看一个更隐蔽的逃逸例:
λg. (g : ∀a. a → a)
本页规则给无注解的λ参数g一个单态未知β。检查内部注解时引入刚性b,要求g:b→b。如果普通合一直接写下β=b→b,就把一个只在内部量词检查期间存在的b塞进了外层g的类型。离开该检查范围后b失去作用域,所以这个解必须拒绝。
正确的修复是把接口明确写成 λ(g:∀a.a→a).g,使g一开始就携带全称方案,而不是借内部注解把一个单态参数升级成任意多态值。实现常为未知量记录创建层级,或在返回时检查自由刚性变量,以发现这种逃逸。
高秩与impredicative不是同义词
把 id:∀t.t→t,把t实例化为
即使两个程序在显式System F中都能表达,面向推断的语言也可能只支持前一种。遇到拒绝时,应先检查是否真的需要多态实例化,而非仅仅缺一个高秩参数注解。
推论与应用
高秩接口可以让库函数重复使用调用者提供的通用操作,也可以把一个类型变量限制在局部计算内部。无论应用场景是什么,检查机制都要保持同一原则:引入∀时使用任意刚性变量,消去∀时才选择实例。
注解驱动的检查把困难集中在边界。知道参数需要
调试时可为每个类型变量记下三个信息:谁绑定它、它是刚性还是可解、哪个作用域允许使用它。例子中的Int/Bool两次实例化与β=b→b逃逸,分别检验实例独立性和作用域保持;仅看最终类型字符串很容易漏掉后者。
参考资料
- Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, Mark Shields, “Practical Type Inference for Arbitrary-Rank Types”, JFP 17(1), 2007, 1–82:高秩接口、skolem化、subsumption及逃逸检查
- OCaml manual, “Polymorphism and its limitations”, §5.3:多态参数与显式接口;具体OCaml通过多态记录字段等语法表达