【问题标题】:Merging two iterables in Scala在 Scala 中合并两个可迭代对象
【发布时间】:2016-06-21 17:31:23
【问题描述】:

我想编写一个merge 方法,它接受两个迭代并将它们合并在一起。 (也许合并不是描述我想要的最好的词,但为了这个问题,它是无关紧要的)。我希望这种方法可以通用以处理不同的具体迭代。

例如,merge(Set(1,2), Set(2,3)) 应该返回 Set(1,2,3)merge(List(1,2), List(2,3)) 应该返回 List(1, 2, 2, 3)。我做了以下天真的尝试,但编译器抱怨res 的类型:它是Iterable[Any] 而不是A

def merge[A <: Iterable[_]](first: A, second: A): A = {
    val res = first ++ second
    res
}

如何解决这个编译错误? (我更感兴趣的是了解如何实现这样的功能,而不是为我完成它的库,因此非常感谢解释为什么我的代码不起作用。)

【问题讨论】:

    标签: scala generics collections


    【解决方案1】:

    让我们从您的代码不起作用的原因开始。首先,您不小心使用了existential type 的缩写语法,而不是实际使用绑定在更高种类类型上的类型。

    // What you wrote is equivalent to this
    def merge[A <: Iterable[T] forSome {type T}](first: A, second: A): A
    

    即使修复它也不能完全满足您的需求。

    def merge[A, S[T] <: Iterable[T]](first: S[A], second: S[A]): S[A] = {
      first ++ second // CanBuildFrom errors :(
    }
    

    这是因为++ 不使用类型边界来实现其多态性,它使用隐式CanBuildFrom[From, Elem, To]CanBuildFrom 负责提供适当的 Builder[Elem, To],这是一个可变缓冲区,我们使用它来构建所需类型的集合。

    这意味着我们将不得不给它它如此渴望的CanBuildFrom 并且一切都会正常工作?

    import collection.generic.CanBuildFrom
    
    // Cannot construct a collection of type S[A] with elements of type A 
    // based on a collection of type Iterable[A]
    merge0[A, S[T] <: Iterable[T], That](x: S[A], y: S[A])
      (implicit bf: CanBuildFrom[S[A], A, S[A]]): S[A] = x.++[A, S[A]](y)
    

    没有:(。

    我在++ 中添加了额外的类型注释,以使编译器错误更加相关。这告诉我们的是,因为我们没有专门用我们自己的S 覆盖Iterable++,所以我们正在使用Iterable 的实现,这恰好发生在采用从 Iterable 构建到我们的 S 的隐式 CanBuildFrom

    这是@ChrisMartin 碰巧遇到的问题(整件事实际上是对他的回答的冗长评论)。

    不幸的是,Scala 不提供这样的CanBuildFrom,所以看起来我们必须手动使用CanBuildFrom

    所以我们去兔子洞……

    让我们首先注意到++ 实际上最初是在TraversableLike 中定义的,因此我们可以使我们的自定义merge 更通用一点。

    def merge[A, S[T] <: TraversableLike[T, S[T]], That](it: S[A], that: TraversableOnce[A])
      (implicit bf: CanBuildFrom[S[A], A, That]): That = ???
    

    现在让我们实际实现该签名。

     import collection.mutable.Builder
    
     def merge[A, S[T] <: TraversableLike[T, S[T]], That](it: S[A], that: TraversableOnce[A])
      (implicit bf: CanBuildFrom[S[A], A, That]): That= {
        // Getting our mutable buffer from CanBuildFrom
        val builder: Builder[A, That] = bf()
        builder ++= it
        builder ++= that
        builder.result()
      }
    

    请注意,我已将 GenTraversableOnce[B]* 更改为 TraversableOnce[B]**。这是因为使Builder++= 工作的唯一方法是顺序访问***。这就是CanBuildFrom 的全部内容。它为您提供了一个可变缓冲区,您可以使用所需的所有值填充该缓冲区,然后您可以使用 result 将缓冲区转换为所需的输出集合。

    scala> merge(List(1, 2, 3), List(2, 3, 4))
    res0: List[Int] = List(1, 2, 3, 2, 3, 4)
    
    scala> merge(Set(1, 2, 3), Set(2, 3, 4))
    res1: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)
    
    scala> merge(List(1, 2, 3), Set(1, 2, 3))
    res2: List[Int] = List(1, 2, 3, 1, 2, 3)
    
    scala> merge(Set(1, 2, 3), List(1, 2, 3)) // Not the same behavior :(
    res3: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
    

    简而言之,CanBuildFrom 机制允许您构建代码来处理我们经常希望在 Scala 集合的继承图的不同分支之间自动转换的事实,但它的代价是一些复杂性并且有时不直观行为。相应地权衡取舍。

    脚注

    * 我们可以“遍历”至少“一次”的“通用”集合,但可能不会更多,以某种顺序可能是顺序的,也可能不是顺序的,例如也许是平行的。

    ** 与GenTraversableOnce 相同,但不是“常规”,因为它保证顺序访问。

    *** TraversableLike 通过在内部强行调用 seq 来解决这个问题 GenTraversableOnce,但我觉得这是在欺骗人们的并行性,而他们本来可以预料到的。强制调用者决定是否要放弃并行性;不要为他们隐形。

    【讨论】:

    • 感谢您的广泛回答。只有一件事:TraversableLike 有两个类型参数:trait TraversableLike[+A, +Repr],我必须将其定义为 S[A] &lt;: TraversableLike[A, S[A]]
    • 这就是我即时进行更改而不验证它们实际编译的结果。我会纠正它,谢谢!
    【解决方案2】:

    首先,这里是此答案中所有代码所需的导入:

    import collection.GenTraversableOnce
    import collection.generic.CanBuildFrom
    

    首先查看the API doc 以查看Iterable.++ 的方法签名(请注意大多数集合are wrong 的API 文档,您需要单击“完整签名”才能查看真实类型):

    def ++[B >: A, That](that: GenTraversableOnce[B])
      (implicit bf: CanBuildFrom[Iterable[A], B, That]): That
    

    从那里您可以直接将实例方法转换为函数:

    def merge[A, B >: A, That](it: Iterable[A], that: GenTraversableOnce[B])
      (implicit bf: CanBuildFrom[Iterable[A], B, That]): That = it ++ that
    

    分解:

    • [A, B &gt;: A, That]Iterable 有一个类型参数A,而++ 有两个类型参数BThat,因此生成的函数具有所有三个类型参数ABThat
    • it: Iterable[A] — 该方法属于Iterable[A],所以我们将第一个值参数设为
    • that: GenTraversableOnce[B])(implicit bf: CanBuildFrom[Iterable[A], B, That]): That — 直接从++ 的签名中复制的剩余参数和类型约束

    【讨论】:

    • 如果我在val x = merge(Set(1, 2, 3), Set(1, 2, 3, 4)) 中运行你的合并定义,x 的类型是Iterable[Int],但我想要Set[Int]。有没有办法找回最具体的类型?
    • 哎呀,我以为是的。绝对有可能,会继续努力。
    • 不,我走投无路了,抱歉。
    猜你喜欢
    • 2019-12-03
    • 2021-11-08
    • 2023-03-07
    • 2022-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-31
    • 2018-05-18
    相关资源
    最近更新 更多