【问题标题】:A diverging implicit expansion in Scala, involving chained implicitsScala 中一个发散的隐式扩展,涉及链式隐式
【发布时间】:2018-03-03 19:29:01
【问题描述】:

(注意:这个问题自 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 implicit


    【解决方案1】:

    您会惊讶地发现自己没有做错任何事!至少在逻辑层面上。您在这里遇到的错误是 Scala 编译器在解析递归数据结构的隐式时的已知行为。在The Type Astronaut's Guide to Shapeless一书中对这种行为进行了很好的解释:

    隐式解析是一个搜索过程。编译器使用启发式方法来确定它是否“收敛”在一个解决方案上。如果启发式不产生 对于特定搜索分支的有利结果,编译器假定 分支没有收敛并移动到另一个分支。

    一种启发式方法是专门为避免无限循环而设计的。如果编译器 在特定搜索分支中两次看到相同的目标类型,它放弃 并继续前进。如果我们看一下扩展,我们可以看到这种情况发生 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],因此编译器移动到 搜索的另一个分支。最终的结果是它无法 找到一个合适的隐式。

    在您的情况下,如果编译器继续运行并且没有这么早放弃,它最终会找到解决方案!但请记住,并非每个发散的隐式错误都是错误的编译器警报。有些实际上是发散/无限扩展的。

    我知道这个问题的两种解决方案:

    1. 递归类型的基于宏的惰性求值

    shapeless 库有一个 Lazy 类型,它对 Hlists 头部到运行时的评估不同,因此可以防止这种发散的隐式错误。我发现解释或提供它的例子超出了 OP 的主题。但你应该检查一下。

    1. 创建隐式检查点,以便编译器事先可以使用递归类型的隐式
    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
    }
    

    如果您不喜欢这两种解决方案,这并不可惜。 shapelessLazy 解决方案虽然经过验证,但仍然是第三方库依赖项,并且在 scala 3.0 中删除了宏,我不确定所有这些基于宏的技术会变成什么。

    【讨论】:

    • 我实际上有一个使用宏的“真实”系统的工作版本。用宏来做这些会带来自己的问题:
    • 我实际上已经阅读了《宇航员指南》,并且我进行了一些代码探索,试图找出是否有办法修补 Scala 以“更加努力”。大概有,但是在代码中找不到简单的参数来调优。
    • @eje 作为对这个主题进行过研究的人,你有没有看到关于这个问题的任何进展?看起来每次你想用 Scala 的隐式做一些非常酷的事情时,这个确切的显示停止器就会弹出......
    • 我什么都没看到(输入宇航员指南或无形代码是最接近的)。可能有更好的方法,但我不介意编译器开关只是禁用“放弃”行为,代价是在真正的分歧上溢出。
    • @eje 有时我认为在这种情况下实现“@NoGiveup”注释会有多困难:P
    猜你喜欢
    • 1970-01-01
    • 2017-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-30
    • 2014-01-01
    • 2021-09-23
    • 2012-03-18
    相关资源
    最近更新 更多