Scala中的发散隐式展开,涉及链式隐式
(注意:从Scala 2.13开始,此问题已修复,请参见此处:) 我正在研究一个涉及链式隐式的Scala类型系统。在许多情况下,该系统的行为与我预期的一样,但在其他情况下,由于分散扩展而失败。到目前为止,我还没有找到一个很好的解释来解释这种分歧,我希望社区能为我解释一下 下面是一个复制问题的简化类型系统:Scala中的发散隐式展开,涉及链式隐式,scala,implicit,Scala,Implicit,(注意:从Scala 2.13开始,此问题已修复,请参见此处:) 我正在研究一个涉及链式隐式的Scala类型系统。在许多情况下,该系统的行为与我预期的一样,但在其他情况下,由于分散扩展而失败。到目前为止,我还没有找到一个很好的解释来解释这种分歧,我希望社区能为我解释一下 下面是一个复制问题的简化类型系统: object repro { import scala.reflect.runtime.universe._ trait +[L, R] case class Atomic[V
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>:load/home/eje/disference-repo.scala
正在加载/home/eje/distance repro.scala。。。
定义模块复制
scala>导入复制_
进口复制品_
scala>隐式[AsString[X]].str
res0:String=X
scala>隐式[AsString[X+Y]].str
res1:String=(X)+(Y)
scala>隐式[AsString[Q]].str
res2:String=((X)+(Y))+(Z)
scala>隐式[AsString[R]].str
:12:错误:类型repro.AsString[repro.R]的发散隐式展开
从对象关联中的方法赋值开始
隐式[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
我们在第1行和第5行中两次看到Tree[A]
,因此编译器继续
搜索的另一个分支。最终的结果是它未能做到这一点
找到合适的方法
在您的情况下,如果编译器一直在运行,并且没有这么早就放弃,那么它最终会找到解决方案!但请记住,并非所有发散的隐式错误都是错误的编译器警报。有些事实上正在发散/无限扩张
我知道这个问题有两种解决方案:
shapeless
库有一个Lazy
类型,该类型将它的Hlist
头的计算结果与运行时不同,因此防止了这种发散的隐式错误。我发现解释或提供示例超出了OP主题的范围。但是你应该检查一下
隐式[AsString[X]].str
隐式[AsString[X+Y]].str
val asQ=隐式[AsString[Q]]
asQ.str
{
隐式val asQImplicitCheckpoint:AsString[Q]=asQ
隐式[AsString[R]].str
}
如果你对这两种解决方案都不感兴趣,那也不可惜。
shapeless
的Lazy
解决方案虽然经过验证,但仍然是第三方库的依赖项,而且随着scala 3.0中宏的删除,我不确定所有这些基于宏的技术会变成什么样子。我实际上有一个“真实”系统的工作版本,使用宏。使用宏执行这些操作会带来一些问题:我实际上已经阅读了《宇航员指南》,我做了一些代码潜水,试图找出是否有办法修补Scala以“更加努力”。可能有,但在代码中找不到一个简单的参数来调整。@eje作为一个在这个主题上做过研究的人,你看到有人在做什么吗?看起来每次你想用Scala的暗含做一些真正酷的事情时,就会弹出一个完全一样的节目塞……我什么都没看到(键入《宇航员指南》或最接近的不成形代码)。可能有更好的方法,但我不介意编译器开关只是禁用“放弃”行为,而代价是在真正的分歧上溢出。@eje有时我想实现一个用于这种情况的“@NoGiveup”注释有多困难:P