“多态递归让递归体内的自调用在不同元素类型上实例化,例如从Perfect a递归到Perfect(a×a)。高秩参数则把全称量词保留在函数箭头左侧,使同一个参数能在一次调用里处理Int与Boo…”
形式陈述
多态递归允许递归函数在自己的定义体中,以同一全称类型方案的不同实例调用自己。它与“函数定义完以后可被外部调用者用于多个类型”不同:变化发生在递归调用尚未结束的内部。
例如给定注解
一个简化的带注解递归检查规则是
其中还要有
普通HM递归规则则通常先给f一个共享单态类型来检查定义体,完成后才对外泛化。这两种规则的差别正是递归体里看到的是一个单态占位符,还是一个可重新实例化的方案。
直觉
递归经常让问题规模变小,但“变小”不一定保持元素类型原封不动。一种非规则数据结构把相邻两个元素先配成一对,再把这些对子作为下一层的元素。向下递归时结构更浅,元素类型却从a变成a×a。
多态递归给函数一个承诺:无论这一层的元素类型是什么,都能处理。每走下一层,重新使用这个承诺,而不是强迫所有层共用同一个元素类型。
例子与边界
非规则Perfect类型
定义代数数据类型
data Perfect a = Leaf a | Node (Perfect (a × a))
构造器签名为
Node的递归参数不是Perfect a,而是Perfect(a×a),所以该类型不是保持参数不变的规则递归类型。定义深度:
depth : ∀a. Perfect a → Int
depth (Leaf _) = 0
depth (Node t) = 1 + depth t
逐分支检查时,设外层任意类型为刚性α。
- 输入是Perfect α,Leaf分支得到元素α;返回0具有Int类型
- Node分支得到
- 递归使用depth时,把其独立量化的a实例化为α×α,得到
- 所以
depth t:Int,加一仍为Int;外层α没有被改写
递归调用实例化的是f方案里的绑定变量,绝不是把当前外层α解成α×α。
单态递归假设为何失败
若先假设
在有限类型树的一阶合一中,β出现在右侧内部,occurs check拒绝。这次失败不是发现程序会在运行时出错,而是指出单态递归假设太窄。已给定全称签名的检查不需要这个方程。
单写 depth:Perfect a→Int、却把a解释为一个待求解变量,也不等于显式声明
一条有具体类型的执行
取
t = Node (Node (Leaf ((1,2),(3,4))))
最内层Leaf的元素类型是
三次调用的元素参数依次为Int、Int×Int、(Int×Int)×(Int×Int),结果类型一直是Int。递归沿一个真正的子结构前进,所以这个depth还可以独立证明终止;多态递归规则本身并不保证所有递归定义终止。
推论与应用
多态递归常见于嵌套数据类型、类型化语法树及携带不同索引实例的遍历。程序员给出一个对各层统一成立的接口,检查器在每次递归使用时选择合适实例。
不带注解的一般多态递归类型推断不可判定;这不是说任何具体程序都无法检查,也不是说必须进行运行时类型测试。实践中常要求递归签名,再在明确的系统限制下检查定义体。与完全自动的HM主类型推断相比,责任从“算法猜出最一般接口”转成“程序员提供接口,算法验证每个分支”。
本例提供了一个可重复诊断方法:先列出递归实参类型,再问它是否必须与外层实参共享同一实例。若结构递归使类型参数由a变成a×a,强行共享会生成自包含方程;若参数始终不变,普通单态递归往往足够。不能见到任何occurs-check失败都归因于需要多态递归,例如自应用 x x在别的假设下仍可能真正无类型。
参考资料
- OCaml manual, “Polymorphism and its limitations”, §5.2:非规则数据类型、显式全称递归注解及失败原因
- Mark P. Jones, Typing Haskell in Haskell, 2000,显式与隐式递归绑定的处理及相关文献
- Fritz Henglein, “Type Inference with Polymorphic Recursion,” TOPLAS 15(2), 1993, 253–289