(注意:此问题从Scala 2.13开始已修复,请参见此处:https : //github.com/scala/scala/pull/6050)
我正在一个涉及链隐式的Scala类型的系统上工作。在许多情况下,该系统的行为均与我预期的一样,但在其他情况下,由于扩展不同而导致失败。到目前为止,我还没有为这种差异提供一个很好的解释,我希望社区能够为我解释一下!
这是一个重现该问题的简化类型系统:
object repro {
import scala.reflect.runtime.universe._
trait +[L, R]
case class Atomic[V](val name: String)
object Atomic {
def apply[V](implicit vtt: TypeTag[V]): Atomic[V] = Atomic[V](vtt.tpe.typeSymbol.name.toString)
}
case class Assign[V, X](val name: String)
object Assign {
def apply[V, X](implicit vtt: TypeTag[V]): Assign[V, X] = Assign[V, X](vtt.tpe.typeSymbol.name.toString)
}
trait AsString[X] {
def str: String
}
object AsString {
implicit def atomic[V](implicit a: Atomic[V]): AsString[V] =
new AsString[V] { val str = a.name }
implicit def assign[V, X](implicit a: Assign[V, X], asx: AsString[X]): AsString[V] =
new AsString[V] { val str = asx.str }
implicit def plus[L, R](implicit asl: AsString[L], asr: AsString[R]): AsString[+[L, R]] =
new AsString[+[L, R]] { val str = s"(${asl.str}) + (${asr.str})" }
}
trait X
implicit val declareX = Atomic[X]
trait Y
implicit val declareY = Atomic[Y]
trait Z
implicit val declareZ = Atomic[Z]
trait Q
implicit val declareQ = Assign[Q, (X + Y) + Z]
trait R
implicit val declareR = Assign[R, Q + Z]
}
以下是行为的演示,其中包含一些工作案例,然后是发散性失败:
scala> :load /home/eje/divergence-repro.scala
Loading /home/eje/divergence-repro.scala...
defined module repro
scala> import repro._
import repro._
scala> implicitly[AsString[X]].str
res0: String = X
scala> implicitly[AsString[X + Y]].str
res1: String = (X) + (Y)
scala> implicitly[AsString[Q]].str
res2: String = ((X) + (Y)) + (Z)
scala> implicitly[AsString[R]].str
<console>:12: error: diverging implicit expansion for type repro.AsString[repro.R]
starting with method assign in object AsString
implicitly[AsString[R]].str
如果您没有做错任何事,您会感到惊讶!至少在逻辑层面上如此。在这里遇到的错误是Scala编译器在解析递归数据结构的隐式时的已知行为。《无形的类型宇航员指南》一书对此行为做了很好的解释:
隐式解析是一个搜索过程。编译器使用试探法确定它是否在解决方案上“收敛”。如果启发式方法不能为特定的搜索分支产生令人满意的结果,则编译器会假定该分支未收敛,而是移至另一个分支。
一种启发式方法专门设计用于避免无限循环。如果编译器在特定的搜索分支中两次看到相同的目标类型,则它放弃并继续前进。如果我们查看的扩展,就会看到这种情况的发生
CsvEncoder[Tree[Int]]
。隐式解析过程经历以下几种类型:
CsvEncoder[Tree[Int]] // 1
CsvEncoder[Branch[Int] :+: Leaf[Int] :+: CNil] // 2
CsvEncoder[Branch[Int]] // 3
CsvEncoder[Tree[Int] :: Tree[Int] :: HNil] // 4
CsvEncoder[Tree[Int]] // 5 uh oh
我们
Tree[A]
在第1行和第5行看到了两次,因此编译器移至另一个搜索分支。最终结果是无法找到合适的隐式。
在您的情况下,如果编译器继续运行并且不放弃得这么早,它将最终达到解决方案!但是请记住,并非每个发散的隐式错误都是错误的编译器警报。有些实际上是发散/无限扩展的。
我知道此问题有两种解决方案:
该shapeless
库的Lazy
类型不同于Hlist
对运行时的评估,因此可以避免这种发散的隐式错误。我发现解释或提供示例超出了OP主题。但您应该检查一下。
implicitly[AsString[X]].str
implicitly[AsString[X + Y]].str
val asQ = implicitly[AsString[Q]]
asQ.str
{
implicit val asQImplicitCheckpoint: AsString[Q] = asQ
implicitly[AsString[R]].str
}
如果您既不喜欢这两种解决方案,也不会感到羞耻。对shapeless
的Lazy
,而尝试和真正的解决方案仍然是一个第三方库的依赖,还具有去除在斯卡拉3.0宏我不能确定什么会成为所有这些基于宏观工艺的。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句