【问题标题】:Why does scalac rise a "diverging implicit expansion" error here?为什么 scalac 会在这里出现“发散隐式扩展”错误?
【发布时间】:2017-02-11 16:36:59
【问题描述】:

在下面的代码中,我尝试使用无形派生类型类实例。但是,在更复杂的案例类(转换为更复杂的 HList)的情况下,编译器给了我一个“发散隐式扩展”,即使它似乎没有两次解析同一种隐式类型。也许我错过了编译器的其他一些规则?

(小提琴:https://scalafiddle.io/sf/WEpnAXN/0

import shapeless._

trait TC[T]

sealed trait Trait1
case class SimpleClass(a: String) extends Trait1

sealed trait Trait2
case class ComplexClass(a: String, b: String) extends Trait2

object Serialization extends App {

    //Instances for HList
    implicit val hnilInstance: TC[HNil] = ???
    implicit def hconsInstance[H, T <: HList] (implicit t: TC[T]): TC[H :: T] = ???

    //Instances for CoProduct
    implicit val cnilInstance: TC[CNil] = ???
    implicit def cconsInstance[H, T <: Coproduct] (implicit h: TC[H], t: TC[T]): TC[H :+: T] = ???

    //Instances for Generic, relying on HNil & HCons
    implicit def genericInstance[T, H] (implicit g: Generic.Aux[T, H], t: TC[H]): TC[T] = ???

    the[TC[SimpleClass :+: CNil]]  //Works
    the[TC[Trait1]]                //Works
    the[TC[ComplexClass :+: CNil]] //Works
    the[TC[Trait2]]                //Fails with diverging implicit expansion
}

当尝试解析 the[TC[Trait1]] 时,编译器应该这样做:

TC[Trait1]
    Generic[Trait1]
    TC[SimpleClass :+: CNil]
        TC[SimpleClass]
            Generic[SimpleClass]
            TC[String :: HNil]
        TC[CNil]

这似乎有效。但是,对于 2-field 案例类,编译器无法执行此类操作 - 所以我想知道:为什么我必须在这里使用 Lazy 才能使其工作?

TC[Trait2]
    Generic[Trait2]
    TC[ComplexClass :+: CNil]
        TC[ComplexClass]
            Generic[ComplexClass]
            TC[String :: String :: HNil]
        TC[CNil]

我已经创建了一些小提琴,所以你可以直接在那里执行代码。

【问题讨论】:

  • 我怀疑 Miles 的回答 here 是解释,尽管情况与我在那个问题中的情况并不完全相同。

标签: scala typeclass implicit shapeless


【解决方案1】:

几年前,当我处理some issues like this 时,我发现找出分歧检查器在做什么的最简单方法就是将一些printlns 放入编译器并在本地发布。在 2.12 中,相关代码是 dominates 方法 here,我们可以将其中的最后一行替换为如下内容:

overlaps(dtor1, dted1) && (dtor1 =:= dted1 || {
  val dtorC = complexity(dtor1)
  val dtedC = complexity(dted1)
  val result = dtorC > dtedC

  println(if (result) "Dominates:" else "Does not dominate:")
  println(s"$dtor (complexity: $dtorC)")
  println(s"$dted (complexity: $dtedC)")
  println("===========================")
  result
})

然后我们可以sbt publishLocal scalac 并尝试编译您的代码:

Dominates:
TC[shapeless.::[String,shapeless.::[String,shapeless.HNil]]] (complexity: 7)
TC[shapeless.:+:[ComplexClass,shapeless.CNil]] (complexity: 6)
===========================

这里的问题是我们正在为String :: String :: HNil(树中的最低节点)寻找TC 实例,但我们对ComplexClass :+: CNil 进行了公开搜索(向上三个步骤)。编译器认为String :: String :: HNilComplexClass :+: CNil 重叠并支配ComplexClass :+: CNil,它会退出,因为这看起来是递归的。

这听起来很荒谬,所以我们可以做一个实验,通过在副产品部分添加一些复杂性并看看会发生什么来说服自己。让我们添加一个构造函数:

case class Foo(i: Int) extends Trait2

现在一切正常,我们在编译期间收到此消息:

Does not dominate:
TC[shapeless.::[String,shapeless.::[String,shapeless.HNil]]] (complexity: 7)
TC[shapeless.:+:[ComplexClass,shapeless.:+:[Foo,shapeless.CNil]]] (complexity: 9)

所以ComplexClass hlist 表示仍然重叠Trait2 coproduct 表示,但它并没有支配它,因为Trait2 表示(开放隐式TC 的主题我们担心的搜索)现在更复杂了。

checker 显然在这里过于偏执,它的行为may change in the future,但现在我们被它困住了。正如您所注意到的,最直接和最简单的解决方法是在其中粘贴 Lazy 以隐藏分歧检查器的假定递归。

不过,在这种情况下,看起来只是将实例放在 TC 伴随对象中也可以:

import shapeless._

sealed trait Trait1
case class SimpleClass(a: String) extends Trait1

sealed trait Trait2
case class ComplexClass(a: String, b: String) extends Trait2

trait TC[T]

object TC {
  //Instances for HList
  implicit def hnilInstance: TC[HNil] = ???
  implicit def hconsInstance[H, T <: HList](implicit t: TC[T]): TC[H :: T] = ???

  //Instances for CoProduct
  implicit def cnilInstance: TC[CNil] = ???
  implicit def cconsInstance[H, T <: Coproduct](implicit
    h: TC[H], t: TC[T]
  ): TC[H :+: T] = ???

  //Instances for Generic, relying on HNil & HCons
  implicit def genericInstance[T, H](implicit
    g: Generic.Aux[T, H], t: TC[H]
  ): TC[T] = ???
}

然后:

Does not dominate:
TC[shapeless.::[String,shapeless.::[String,shapeless.HNil]]] (complexity: 7)
TC[shapeless.:+:[ComplexClass,shapeless.CNil]] (complexity: 16)

为什么像这样移动东西会增加副产品的复杂性?我不知道。

【讨论】:

  • 对这些错误的解释更多:issues.scala-lang.org/browse/SI-8467
  • 我没有看到那个问题,@SethTisue,但听起来它只是限制-Xlog-implicits 的输出,并且仅仅拥有当前消息的更具体的子集并没有真正发生帮助解决此类奇怪的分歧案例。
  • 很好的解释!尽管我们俩似乎仍然不明白为什么编译器如此偏执,但至少故障点现在非常清楚。谢谢!
  • @valenterry 是的,不幸的是,我认为 why 可能只是“因为这种行为大多未指定,并且在某些时候编译器开发人员认为大多数情况下还好就足够了”。
猜你喜欢
  • 1970-01-01
  • 2018-10-09
  • 2011-06-20
  • 2017-12-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-17
  • 2019-02-07
相关资源
最近更新 更多