Scala2中用隐式实现匹配类型:逆变标记引发的问题及修复
当给FnUnpack添加逆变标记-F后,其类型子关系发生了本质变化:若F1 <: F2,则FnUnpack[F2] <: FnUnpack[F1]。这意味着,当你请求FnUnpack[Int => String]类型的隐式实例时,编译器可以匹配任何FnUnpack[F](只要Int => String <: F,因为FnUnpack[F]是FnUnpack[Int => String]的子类型,可向上转型)。
结合Function1[-A, +B]的子类型规则,Int => String <: F等价于F是某个I => O,其中:
I <: Int(Function1的输入参数是逆变的,更宽泛的输入类型能兼容更窄的输入)String <: O(Function1的输出参数是协变的,更窄的输出类型能兼容更宽泛的输出)
编译器会选择最一般化的类型推断结果:I=Nothing(所有类型的子类型,满足I <: Int)、O=Any(所有类型的超类型,满足String <: O),最终导致u.II被推断为Nothing,无法通过u.II =:= Int的校验。
而原代码无逆变标记时,FnUnpack是不变类型,只有FnUnpack[Int => String]本身能匹配请求,编译器只能推断I=Int、O=String,因此可以正常工作。
方案1:添加精确类型等式约束
通过=:=约束强制隐式实例中的I => O严格等于目标F类型,避免编译器选择过于宽泛的I和O:
{ trait FnUnpack[-F <: Function1[?, ?]] { type II type OO } object FnUnpack { implicit def only[F <: Function1[?, ?], I, O](implicit ev: F =:= (I => O)): FnUnpack[F] { type II = I; type OO = O } = null } val u = summon[FnUnpack[Int => String]] summon[u.II =:= Int] // 编译通过 }
这个约束确保F必须精确等于I => O,因此请求FnUnpack[Int => String]时,I只能是Int,O只能是String,u.II自然被推断为Int。
方案2:分离类型解析逻辑
将函数类型的输入输出解析逻辑放到独立的不变类型类中,避免逆变对类型推断的干扰:
{ trait Function1Shape[F <: Function1[?, ?]] { type In type Out } object Function1Shape { implicit def define[I, O]: Function1Shape[I => O] { type In = I; type Out = O } = null } trait FnUnpack[-F <: Function1[?, ?]] { type II = Function1Shape[F]#In type OO = Function1Shape[F]#Out } object FnUnpack { implicit def instance[F <: Function1[?, ?]](implicit shape: Function1Shape[F]): FnUnpack[F] = null } val u = summon[FnUnpack[Int => String]] summon[u.II =:= Int] // 编译通过 }
这里Function1Shape作为不变类型类负责精确解析函数类型,FnUnpack依赖它获取类型成员,既保留了逆变特性,又保证了类型推断的准确性。
内容的提问来源于stack exchange,提问作者tribbloid

