Scala中的发散隐式展开,涉及链式隐式

Scala中的发散隐式展开,涉及链式隐式,scala,implicit,Scala,Implicit,(注意:从Scala 2.13开始,此问题已修复,请参见此处:) 我正在研究一个涉及链式隐式的Scala类型系统。在许多情况下,该系统的行为与我预期的一样,但在其他情况下,由于分散扩展而失败。到目前为止,我还没有找到一个很好的解释来解释这种分歧,我希望社区能为我解释一下 下面是一个复制问题的简化类型系统: object repro { import scala.reflect.runtime.universe._ trait +[L, R] case class Atomic[V

(注意:从Scala 2.13开始,此问题已修复,请参见此处:)

我正在研究一个涉及链式隐式的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>: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