Scala非有限类图下类型层面协变返回类型实现方案咨询
设想一段十分常见的Scala代码:
trait Template[X, +T] { def m: T } trait Root[X] extends Template[X, Wrap[X]] { override def m = new Wrap[X] } trait Sub[X] extends Root[X] with Template[X, Wrap2[X]] { override def m = new Wrap2[X] } trait Wrap[X] extends Root[Option[X]] trait Wrap2[X] extends Sub[X]
Root是某类层级的基类,其子类(例如Sub)会按需收窄方法m的返回类型,符合协变返回类型的常规语义。额外需求为:需要将该返回类型以某种形式编码到所有满足C <: Root[_]的类型中,以便基于此做泛化抽象。这一需求通过Template特质实现:可以在隐式解析中声明类型参数[X, T, W <: Template[X, T]](此处暂不讨论类型推断相关问题)。
上述代码无法通过编译,核心问题是类图是非有限的,原因如下:
Root[X] <: Template[X, Wrap[X]] <: Template[X, Root[Option[X]]]
编译器会递归替换展开Root[Option[X]]最终导致无限展开。
目前已尝试的两种方案均存在缺陷:
- 方案1:将
Template定义为高阶类型
该方案不可行,因为返回类型的种类(kind)并不总是一致:哪怕仅为类型参数添加一个边界都会导致该方案失效。trait Template[X, +T[_]] { def m: T[X] } - 方案2:使用抽象类型成员
该方案的缺陷是:要实现协变,只能将返回类型trait Root[X] { type Result <: Wrap[X] def m: Wrap[X] } trait Sub[X] extends Root[Option[X]] { type Result <: Wrap2[X] }Result声明为上界形式,此时无法在当前特质中实现方法m(除非抛出异常),因为Result在该位置是未确定的抽象类型。如果要保持类层级的可扩展性,就必须为每一个接口特质配套单独的实现类:
即便将实现逻辑抽取为可混入的特质,避免每个需要使用默认实现的class RootImpl[X] extends Root[X] { override type Result = Wrap[X] override def m = new Wrap[X] } class SubImpl[X] extends Sub[X] { override type Result = Wrap2[X] override def m = new Wrap2[X] }Root子类重复编写代码,相比直接在接口特质中编写实现,这种方式仍然十分不便。
方案1:使用F-bounded自类型切断递归展开
核心思路是给基类增加一个代表自身类型的F-bounded参数,显式告知编译器递归边界,避免无限制展开子类型关系,同时保留直接在特质中写默认实现的能力:
trait Template[X, +T] { def m: T } // 增加Self F-bounded参数,绑定当前实际实现类型 trait Root[X, Self <: Root[X, Self]] extends Template[X, Wrap[X]] { // 自类型约束保证Wrap[X]是当前层级的合法子类型 this: Self => override def m: Wrap[X] = new Wrap[X] } trait Sub[X, Self <: Sub[X, Self]] extends Root[X, Self] with Template[X, Wrap2[X]] { this: Self => override def m: Wrap2[X] = new Wrap2[X] } // Wrap和Wrap2实现时传入自身作为Self参数 trait Wrap[X] extends Root[Option[X], Wrap[X]] trait Wrap2[X] extends Sub[X, Wrap2[X]]
这个方案完全保留协变返回类型的语义,不需要额外拆分为接口+实现类,所有默认方法可以直接写在特质里,泛型抽象时可以直接通过Template[X, T]拿到返回类型T,不会触发编译器的无限递归展开。唯一的成本是每个层级的特质需要多带一个F-bounded的Self类型参数,实际使用时如果不需要暴露Self参数,可以在伴生对象里加类型别名简化调用。
方案2:使用类型类映射返回类型
如果不想修改原有类层级的泛型签名,可以把返回类型的映射逻辑从继承链里抽出来,放到单独的类型类里,完全避免继承时的子类型递归检查:
// 原有类层级去掉对Template的继承依赖 trait Root[X] { def m: Wrap[X] = new Wrap[X] } trait Sub[X] extends Root[X] { override def m: Wrap2[X] = new Wrap2[X] } trait Wrap[X] extends Root[Option[X]] trait Wrap2[X] extends Sub[X] // 单独定义类型类做返回类型映射,替代原Template的作用 trait ReturnType[C, X] { type T def getM(c: C): T } // 为每个具体类型提供上下文实例 given [X]: ReturnType[Root[X], X] with { type T = Wrap[X] def getM(c: Root[X]) = c.m } given [X]: ReturnType[Sub[X], X] with { type T = Wrap2[X] def getM(c: Sub[X]) = c.m }
这个方案完全不侵入原有类层级,不需要修改继承关系,也不会触发任何递归展开问题,泛型抽象时直接通过ReturnType[W, X]#T拿到对应的返回类型即可。缺点是需要为新增的子类单独提供对应的ReturnType实例,不过如果是固定的类层级,一次性写好实例后后续使用成本很低。
补充说明:
Root[X] <: Template[X, Root[X]]不会触发展开的原因,是编译器检查到右侧的Root[X]和左侧的Root[X]是同一个类型应用,直接判定相等终止递归;而原代码里右侧是Root[Option[X]],类型参数和左侧不同,编译器会继续深入检查子类型关系,直到栈溢出。
内容的提问来源于stack exchange,提问作者Turin

