在 Traversable 上抽象时的发散隐式扩展

Divergent implicit expansion when abstracting over Traversable

最终编辑:所有极端情况都已解决,唯一的问题是我必须从 Circe 复制私有 Encoder.encodeTraversableOnce 方法才能使 Encoder 正常工作。我还必须更改 MyCollection 以使用 TraversableOnce 而不仅仅是 Traversable(这是因为 Encoder 仅适用于 TraversableOnce,而 DecoderTraversable 一起工作。可以在此处找到演示所有情况的 fiddle https://scalafiddle.io/sf/F5Qo8cn/15

Fiddle 可以在这里找到 https://scalafiddle.io/sf/F5Qo8cn/8

基本上我试图抽象集合类型,这是在包含可遍历集合的模型的上下文中,即假设我们有以下内容

case class MyCollection[C[A] <: Traversable[A]](stuff: C[String])

这允许我们用特定的集合类型实例化 MyCollection,即

val innerV = MyCollection(Vector("a"))
val innerL = MyCollection(List("b"))

MyCollection 也恰好有一个具体类型,所以当我们访问 .stuff 方法时,它将 return 我们用来创建它的类型(即在这种情况下innerVVector 其中 innerLList)

因为这是一个web框架的context,MyCollection恰好代表了一些JSON,所以使用Circe 0.9.1我们可以写一个解码器如下

object MyCollection {

  implicit def decoder[C[A] <: Traversable[A]]: Decoder[MyCollection[C]] = {
    new Decoder[MyCollection[C]] {
      override def apply(c: HCursor) = {
        c.downField("stuff").as[C[String]](Decoder.decodeTraversable[String, C](
          implicitly,
          implicitly
        ))
      }.map((x: C[String]) => MyCollection.apply(x))
    }
  }
}

请注意,我们正在显式调用 implicit 参数并手动编写解码器,以便我们可以帮助跟踪隐式问题出在哪里。这个想法是我们可以用我们想要的任何集合类型来一般地实例化一个 case class,即

def getMyCollection[C[A] <: Traversable[A]]: MyCollection[C] = {
  val jsonString = """{ "stuff": ["a","b"] }"""
  val json = io.circe.parser.parse(jsonString).right.get
  json.as[MyCollection[C]].right.get
}

def asVector: MyCollection[Vector] = getMyCollection[Vector]
def asList: MyCollection[List] = getMyCollection[List]

问题是我得到了不同的隐式扩展,特别是在这一行

c.downField("stuff").as[C[String]](Decoder.decodeTraversable[String, C](
  implicitly,
  implicitly // <- error happens here, this is a CBF implicit
))

我们得到的错误是

ScalaFiddle.scala:19: error: ambiguous implicit values: both getter StringCanBuildFrom in module class Predef of type => generic.this.CanBuildFrom[String,scala.this.Char,String] and method $conforms in module class Predef of type [A]=> $less$colon$less[A,A] match expected type T implicitly ^

有谁知道是什么导致了这个问题

C 的上限太宽松:在方法体内,编译器除了 C 是一个 Traversable[A] 之外,对 C 一无所知,因此它不能自动证明存在CanBuildFrom[Nothing, A, C[A]].

的实例

简单的解决方法是从外部提供 CanBuildFrom[Nothing, A, C[A]],因为这些东西很容易在使用现场生成(因为它显然可以针对 List 这样的具体实现来完成和 Vector):

// Start writing your ScalaFiddle code here
import io.circe._
import io.circe.syntax._
import scala.collection.generic.CanBuildFrom

case class MyCollection[C[A] <: Traversable[A]](stuff: C[String])


val innerV = MyCollection(Vector("a")).stuff
val innerL = MyCollection(List("b")).stuff

object MyCollection {

  implicit def decoder[C[A] <: Traversable[A]]
    (implicit cbf: CanBuildFrom[Nothing, String, C[String]])
  : Decoder[MyCollection[C]] = {
    new Decoder[MyCollection[C]] {
      override def apply(c: HCursor) = {
        c.downField("stuff").as[C[String]](Decoder.decodeTraversable[String, C](
          implicitly,
          // this thing cannot be generated 
          // if you know nothing about `C` except
          // that it is a `Traversable[A]`
          cbf
        ))
      }.map((x: C[String]) => MyCollection.apply(x))
    }
  }
}

def getMyCollection[C[A] <: Traversable[A]]
  (implicit cbf: CanBuildFrom[Nothing, String, C[String]])
: MyCollection[C] = {
  val jsonString = """{ "stuff": ["a","b"] }"""
  val json = io.circe.parser.parse(jsonString).right.get
  json.as[MyCollection[C]].right.get
}

// cbf is supplied by compiler, it is trivial to
// generate here, because you know that you can do it
// for lists and vectors
def asVector: MyCollection[Vector] = getMyCollection[Vector] 
def asList: MyCollection[List] = getMyCollection[List] 

println(asVector)
println(asList)

编辑: 正如@OlegPyzhcov 指出的那样,下面的提议无法工作,因为我们没有可以调用 GenTraversable 的实例 companion。我就放在这里吧,以防万一我突然想起来我在想什么。

Another solution I can think of would be to tighten the upper bound to GenTraversable[A], and then go over the companion to GenericCompanion, and build the required CanBuildFrom[Nothing, String, C[String]] using the newBuilder method.

The tradeoff would be: this would change the bound from Traversable[A] to tighter GenTraversable[A], but then you could drop the annoying cbf-implicit.

注意:Scala 编译器选项 -Xlog-implicits 有时可以帮助您了解正在发生的事情(尽管这种情况 很奇怪!)。

您的范围内没有隐式 CBF。您需要从顶层传递它,其中 C[_] 的具体类型是已知的。所以修复是:

implicit def decoder[C[A] <: Traversable[A]](implicit cbf: CanBuildFrom[Nothing, String, C[String]]): Decoder[MyCollection[C]]

如果你删除 implicitly 语句,你会得到一个不同的错误,它应该提示你你错过了它。

Cannot construct a collection of type C[String] with elements of type String based on a collection of type Nothing.

not enough arguments for method decodeTraversable: (implicit decodeA: io.circe.Decoder[String], implicit cbf: scala.collection.generic.CanBuildFrom[Nothing,String,C[String]])io.circe.Decoder[C[String]].
Unspecified value parameter cbf.

接下来的部分主要是假设,所以请持保留态度。也许一些 Scala 编译器黑客会纠正我。

引发错误的原因在于 expected type T 的来源。 T 未作为类型参数出现在 Decoder 中,也未出现在 CanBuildFrom 中。但是用在scala.Predef:

 @inline def implicitly[T](implicit e: T) = e

所以当你使用 implicitly 时,编译器可以尝试两件事:

  • 根据 Decoder.decodeTraversable
  • 的指示发现 CanBuildFrom 的实例
  • (低优先级)发现某物隐式可用,然后尝试找到隐式转换以获得CanBuildFrom

第一步失败(没有CanBuildFrom)。但这还没有结束。所以编译器试图在没有任何类型 T 约束的情况下找到 implicit e: T。它找到了多个东西:

  • StringCanBuildFrom
  • $conforms

因此,一旦它发现了不同的隐式,它就会立即退出,给你的错误消息没有什么帮助。

修复后,我建议使用 implicitly 或不使用都没有区别,因为它在第一步完成。