Scala中的离散隐式扩展,涉及链式隐式

(注意:此问题从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
shayan

如果您没有做错任何事,您会感到惊讶!至少在逻辑层面上如此。在这里遇到的错误是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行看到了两次,因此编译器移至另一个搜索分支。最终结果是无法找到合适的隐式。

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

我知道此问题有两种解决方案:

  1. 基于宏的递归类型的惰性评估

shapeless库的Lazy类型不同于Hlist对运行时的评估,因此可以避免这种发散的隐式错误。我发现解释或提供示例超出了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,而尝试和真正的解决方案仍然是一个第三方库的依赖,还具有去除在斯卡拉3.0宏我不能确定什么会成为所有这些基于宏观工艺的。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章