Scala中的隐式扩展分歧,涉及链式隐含

时间:2018-03-03 19:29:02

标签: scala implicits scala-implicits

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

1 个答案:

答案 0 :(得分:3)

你知道你没有做错任何事都会感到惊讶!至少在逻辑层面上。您在此处遇到的错误是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类型,不同于Hlist对运行时的评估,因此可以防止这种分歧的隐式错误。我发现解释或提供它的例子超出了OP主题。但你应该检查一下。

  2. 创建隐式检查点,以便预先为编译器提供递归类型的隐式

  3. 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解决方案虽然尝试过,但仍然是第三方库依赖项,并且还删除了scala 3.0中的宏我不知道是什么&#39; ll成为所有这些基于宏观的技​​术。

相关问题