如何将丰富我的库模式应用于 Scala 集合?

Posted

技术标签:

【中文标题】如何将丰富我的库模式应用于 Scala 集合?【英文标题】:How do I apply the enrich-my-library pattern to Scala collections? 【发布时间】:2011-07-21 14:48:42 【问题描述】:

Scala 中最强大的模式之一是enrich-my-library* 模式,它使用隐式转换appear 将方法添加到现有类,而不需要动态方法解析。例如,如果我们希望所有字符串都有方法 spaces 来计算它们有多少个空白字符,我们可以:

class SpaceCounter(s: String) 
  def spaces = s.count(_.isWhitespace)

implicit def string_counts_spaces(s: String) = new SpaceCounter(s)

scala> "How many spaces do I have?".spaces
res1: Int = 5

不幸的是,这种模式在处理泛型集合时遇到了麻烦。例如,有人问了很多关于grouping items sequentially with collections 的问题。没有任何东西可以一次性使用,所以这似乎是使用泛型集合C 和泛型元素类型A 的丰富我的库模式的理想候选者:

class SequentiallyGroupingCollection[A, C[A] <: Seq[A]](ca: C[A]) 
  def groupIdentical: C[C[A]] = 
    if (ca.isEmpty) C.empty[C[A]]
    else 
      val first = ca.head
      val (same,rest) = ca.span(_ == first)
      same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
    
  

当然,除了它不起作用。 REPL 告诉我们:

<console>:12: error: not found: value C
               if (ca.isEmpty) C.empty[C[A]]
                               ^
<console>:16: error: type mismatch;
 found   : Seq[Seq[A]]
 required: C[C[A]]
                 same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
                      ^

有两个问题:我们如何从一个空的C[A] 列表(或凭空而来)获得C[C[A]]?我们如何从same +: 行返回C[C[A]] 而不是Seq[Seq[A]]

* 以前称为 pimp-my-library。

【问题讨论】:

好问题!而且,更好的是,它带有一个答案! :-) @Daniel - 我不反对提供两个或更多答案! 算了,伙计。每当我需要做这样的事情时,我都会给它添加书签以查找它。 :-) 【参考方案1】:

理解这个问题的关键是要意识到在集合库中有两种不同的方式来构建和使用集合。一个是公共集合接口及其所有不错的方法。另一个在创建 collections 库中被广泛使用,但几乎从未在它之外使用过,它是builders。

我们在丰富方面的问题与集合库本身在尝试返回相同类型的集合时所面临的问题完全相同。也就是说,我们想要构建集合,但是在一般工作时,我们没有办法引用“集合已经是相同的类型”。所以我们需要建设者

现在的问题是:我们从哪里获得构建器?明显的地方来自收藏本身。 这不起作用。我们已经决定,在转向泛型集合时,我们将忘记集合的类型。因此,即使集合可以返回一个生成器来生成更多我们想要的类型的集合,它也不知道类型是什么。

相反,我们从漂浮的CanBuildFrom 隐式中获取我们的构建器。这些专门用于匹配输入和输出类型并为您提供适当类型的构建器。

所以,我们有两个概念上的飞跃:

    我们使用的不是标准集合操作,而是构建器。 我们从隐含的CanBuildFroms 中获取这些构建器,而不是直接从我们的集合中获取。

让我们看一个例子。

class GroupingCollection[A, C[A] <: Iterable[A]](ca: C[A]) 
  import collection.generic.CanBuildFrom
  def groupedWhile(p: (A,A) => Boolean)(
    implicit cbfcc: CanBuildFrom[C[A],C[A],C[C[A]]], cbfc: CanBuildFrom[C[A],A,C[A]]
  ): C[C[A]] = 
    val it = ca.iterator
    val cca = cbfcc()
    if (!it.hasNext) cca.result
    else 
      val as = cbfc()
      var olda = it.next
      as += olda
      while (it.hasNext) 
        val a = it.next
        if (p(olda,a)) as += a
        else  cca += as.result; as.clear; as += a 
        olda = a
      
      cca += as.result
    
    cca.result
  

implicit def iterable_has_grouping[A, C[A] <: Iterable[A]](ca: C[A]) = 
  new GroupingCollection[A,C](ca)

让我们把它拆开。首先,为了构建集合的集合,我们知道我们需要构建两种类型的集合:C[A] 用于每个组,C[C[A]] 将所有组聚集在一起。因此,我们需要两个构建器,一个采用As 并构建C[A]s,另一个采用C[A]s 并构建C[C[A]]s。查看CanBuildFrom的类型签名,我们看到

CanBuildFrom[-From, -Elem, +To]

这意味着 CanBuildFrom 想知道我们开始使用的集合的类型——在我们的例子中,它是 C[A],然后是生成的集合的元素和该集合的类型。所以我们将它们填充为隐式参数cbfcccbfc

意识到这一点,这就是大部分工作。我们可以使用我们的CanBuildFroms 为我们提供构建器(您需要做的就是应用它们)。并且一个构建器可以使用+= 建立一个集合,将其转换为最终应该与result 一起使用的集合,然后清空自身并准备以clear 重新开始。构建器一开始是空的,这解决了我们的第一个编译错误,并且由于我们使用构建器而不是递归,第二个错误也消失了。

最后一个小细节——除了实际工作的算法——是在隐式转换中。请注意,我们使用new GroupingCollection[A,C] 而不是[A,C[A]]。这是因为类声明是针对带有一个参数的C,它用传递给它的A 填充它自己。所以我们只需将类型C 交给它,并让它从中创建C[A]。次要细节,但如果您尝试其他方式,则会出现编译时错误。

在这里,我使该方法比“相等元素”集合更通用一些——相反,只要对顺序元素的测试失败,该方法就会将原始集合分开。

让我们看看我们的方法在起作用:

scala> List(1,2,2,2,3,4,4,4,5,5,1,1,1,2).groupedWhile(_ == _)
res0: List[List[Int]] = List(List(1), List(2, 2, 2), List(3), List(4, 4, 4), 
                             List(5, 5), List(1, 1, 1), List(2))

scala> Vector(1,2,3,4,1,2,3,1,2,1).groupedWhile(_ < _)
res1: scala.collection.immutable.Vector[scala.collection.immutable.Vector[Int]] =
  Vector(Vector(1, 2, 3, 4), Vector(1, 2, 3), Vector(1, 2), Vector(1))

有效!

唯一的问题是我们通常没有可用于数组的这些方法,因为这需要连续进行两次隐式转换。有几种方法可以解决这个问题,包括为数组编写单独的隐式转换、转换为 WrappedArray 等等。


编辑:我最喜欢的处理数组和字符串等的方法是使代码更通用,然后使用适当的隐式转换再次使它们更具体,从而使数组工作还。在这种特殊情况下:

class GroupingCollection[A, C, D[C]](ca: C)(
  implicit c2i: C => Iterable[A],
           cbf: CanBuildFrom[C,C,D[C]],
           cbfi: CanBuildFrom[C,A,C]
) 
  def groupedWhile(p: (A,A) => Boolean): D[C] = 
    val it = c2i(ca).iterator
    val cca = cbf()
    if (!it.hasNext) cca.result
    else 
      val as = cbfi()
      var olda = it.next
      as += olda
      while (it.hasNext) 
        val a = it.next
        if (p(olda,a)) as += a
        else  cca += as.result; as.clear; as += a 
        olda = a
      
      cca += as.result
    
    cca.result
  

在这里,我们添加了一个隐式函数,它为我们提供了来自 CIterable[A]——对于大多数集合来说,这只是标识(例如,List[A] 已经是 Iterable[A]),但对于数组,它将是真正的隐式转换。因此,我们已经放弃了C[A] &lt;: Iterable[A] 的要求——我们基本上只是明确了&lt;% 的要求,所以我们可以随意显式使用它,而不是让编译器为我们填充它。此外,我们放宽了集合集合为C[C[A]] 的限制——取而代之的是任何D[C],我们稍后将填写为我们想要的。因为我们稍后会填写它,所以我们将它推到了类级别而不是方法级别。否则基本一样。

现在的问题是如何使用它。对于常规收藏,我们可以:

implicit def collections_have_grouping[A, C[A]](ca: C[A])(
  implicit c2i: C[A] => Iterable[A],
           cbf: CanBuildFrom[C[A],C[A],C[C[A]]],
           cbfi: CanBuildFrom[C[A],A,C[A]]
) = 
  new GroupingCollection[A,C[A],C](ca)(c2i, cbf, cbfi)

现在我们将C[A] 插入CC[C[A]] 插入D[C]。请注意,我们在调用new GroupingCollection 时确实需要显式泛型类型,这样它就可以清楚地知道哪些类型对应于什么。感谢implicit c2i: C[A] =&gt; Iterable[A],它会自动处理数组。

但是等等,如果我们想使用字符串呢?现在我们遇到了麻烦,因为你不能有一个“字符串”。这就是额外的抽象有帮助的地方:我们可以调用D 一些适合保存字符串的东西。让我们选择Vector,然后执行以下操作:

val vector_string_builder = (
  new CanBuildFrom[String, String, Vector[String]] 
    def apply() = Vector.newBuilder[String]
    def apply(from: String) = this.apply()
  
)

implicit def strings_have_grouping(s: String)(
  implicit c2i: String => Iterable[Char],
           cbfi: CanBuildFrom[String,Char,String]
) = 
  new GroupingCollection[Char,String,Vector](s)(
    c2i, vector_string_builder, cbfi
  )

我们需要一个新的CanBuildFrom 来处理字符串向量的构建(但这真的很简单,因为我们只需要调用Vector.newBuilder[String]),然后我们需要填写所有类型,以便GroupingCollection 输入得当。请注意,我们已经在 [String,Char,String] CanBuildFrom 周围浮动,所以字符串可以由字符集合组成。

让我们试试吧:

scala> List(true,false,true,true,true).groupedWhile(_ == _)
res1: List[List[Boolean]] = List(List(true), List(false), List(true, true, true))

scala> Array(1,2,5,3,5,6,7,4,1).groupedWhile(_ <= _) 
res2: Array[Array[Int]] = Array(Array(1, 2, 5), Array(3, 5, 6, 7), Array(4), Array(1))

scala> "Hello there!!".groupedWhile(_.isLetter == _.isLetter)
res3: Vector[String] = Vector(Hello,  , there, !!)

【讨论】:

您可以使用 @Anonymous - 有人会怀疑。但是在这种情况下你试过了吗? @Rex:“要求连续进行两次隐式转换”让我想起了***.com/questions/5332801/…这里适用吗? @Peter - 很有可能!不过,我倾向于编写显式隐式转换,而不是依赖 基于@Peters 评论,我尝试为数组添加另一个隐式转换,但失败了。我真的不明白在哪里添加视图边界。 @Rex,您能否编辑您的答案并展示如何让代码与数组一起使用?【参考方案2】:

从this commit 开始,“丰富”Scala 集合比 Rex 给出出色答案时要容易得多。对于简单的情况,它可能看起来像这样,

import scala.collection.generic. CanBuildFrom, FromRepr, HasElem 
import language.implicitConversions

class FilterMapImpl[A, Repr](val r : Repr)(implicit hasElem : HasElem[Repr, A]) 
  def filterMap[B, That](f : A => Option[B])
    (implicit cbf : CanBuildFrom[Repr, B, That]) : That = r.flatMap(f(_).toSeq)


implicit def filterMap[Repr : FromRepr](r : Repr) = new FilterMapImpl(r)

它为所有GenTraversableLikes 添加了一个尊重filterMap 操作的“相同结果类型”,

scala> val l = List(1, 2, 3, 4, 5)
l: List[Int] = List(1, 2, 3, 4, 5)

scala> l.filterMap(i => if(i % 2 == 0) Some(i) else None)
res0: List[Int] = List(2, 4)

scala> val a = Array(1, 2, 3, 4, 5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a.filterMap(i => if(i % 2 == 0) Some(i) else None)
res1: Array[Int] = Array(2, 4)

scala> val s = "Hello World"
s: String = Hello World

scala> s.filterMap(c => if(c >= 'A' && c <= 'Z') Some(c) else None)
res2: String = HW

对于问题中的示例,解决方案现在看起来像,

class GroupIdenticalImpl[A, Repr : FromRepr](val r: Repr)
  (implicit hasElem : HasElem[Repr, A]) 
  def groupIdentical[That](implicit cbf: CanBuildFrom[Repr,Repr,That]): That = 
    val builder = cbf(r)
    def group(r: Repr) : Unit = 
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if(!rest.isEmpty)
        group(rest)
    
    if(!r.isEmpty) group(r)
    builder.result
  


implicit def groupIdentical[Repr : FromRepr](r: Repr) = new GroupIdenticalImpl(r)

REPL 会话示例,

scala> val l = List(1, 1, 2, 2, 3, 3, 1, 1)
l: List[Int] = List(1, 1, 2, 2, 3, 3, 1, 1)

scala> l.groupIdentical
res0: List[List[Int]] = List(List(1, 1),List(2, 2),List(3, 3),List(1, 1))

scala> val a = Array(1, 1, 2, 2, 3, 3, 1, 1)
a: Array[Int] = Array(1, 1, 2, 2, 3, 3, 1, 1)

scala> a.groupIdentical
res1: Array[Array[Int]] = Array(Array(1, 1),Array(2, 2),Array(3, 3),Array(1, 1))

scala> val s = "11223311"
s: String = 11223311

scala> s.groupIdentical
res2: scala.collection.immutable.IndexedSeq[String] = Vector(11, 22, 33, 11)

再次注意,同样的结果类型原则已被观察到,与在 GenTraversableLike 上直接定义 groupIdentical 的方式完全相同。

【讨论】:

耶!有更多个神奇的片段可以跟踪这种方式,但它们都很好地结合在一起!不必担心每个非集合层次的集合是一种解脱。 太糟糕了,迭代器被无偿排除,因为我的单行更改被拒绝了。 “错误:找不到 scala.collection.generic.FromRepr[Iterator[Int]] 类型的证据参数的隐式值” 哪一行更改被拒绝了? groups.google.com/group/scala-language/browse_thread/thread/… 我在 master 中没有看到这个;它是蒸发了,还是最终进入了 2.10.0 后的分支,或者......?【参考方案3】:

截至this commit,魔法咒语与迈尔斯给出出色答案时的内容略有不同。

以下有效,但它是规范的吗?我希望其中一个大炮能纠正它。 (或者更确切地说,大炮,大炮之一。)如果视图边界是上限,那么您将失去对 Array 和 String 的应用程序。边界是 GenTraversableLike 还是 TraversableLike 似乎并不重要;但是 IsTraversableLike 给你一个 GenTraversableLike。

import language.implicitConversions
import scala.collection. GenTraversable=>GT, GenTraversableLike=>GTL, TraversableLike=>TL 
import scala.collection.generic. CanBuildFrom=>CBF, IsTraversableLike=>ITL 

class GroupIdenticalImpl[A, R <% GTL[_,R]](val r: GTL[A,R]) 
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = 
    val builder = cbf(r.repr)
    def group(r: GTL[_,R]) 
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    
    if (!r.isEmpty) group(r)
    builder.result
  


implicit def groupIdentical[A, R <% GTL[_,R]](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

有不止一种方法可以给有九条生命的猫剥皮。这个版本说,一旦我的源转换为 GenTraversableLike,只要我可以从 GenTraversable 构建结果,就可以这样做。我对我的旧代表不感兴趣。

class GroupIdenticalImpl[A, R](val r: GTL[A,R]) 
  def groupIdentical[That](implicit cbf: CBF[GT[A], GT[A], That]): That = 
    val builder = cbf(r.toTraversable)
    def group(r: GT[A]) 
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    
    if (!r.isEmpty) group(r.toTraversable)
    builder.result
  


implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

第一次尝试包括将Repr 转换为GenTraversableLike 的丑陋。

import language.implicitConversions
import scala.collection. GenTraversableLike 
import scala.collection.generic. CanBuildFrom, IsTraversableLike 

type GT[A, B] = GenTraversableLike[A, B]
type CBF[A, B, C] = CanBuildFrom[A, B, C]
type ITL[A] = IsTraversableLike[A]

class FilterMapImpl[A, Repr](val r: GenTraversableLike[A, Repr])  
  def filterMap[B, That](f: A => Option[B])(implicit cbf : CanBuildFrom[Repr, B, That]): That = 
    r.flatMap(f(_).toSeq)
 

implicit def filterMap[A, Repr](r: Repr)(implicit fr: ITL[Repr]): FilterMapImpl[fr.A, Repr] = 
  new FilterMapImpl(fr conversion r)

class GroupIdenticalImpl[A, R](val r: GT[A,R])(implicit fr: ITL[R])  
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That =  
    val builder = cbf(r.repr)
    def group(r0: R)  
      val r = fr conversion r0
      val first = r.head
      val (same, other) = r.span(_ == first)
      builder += same
      val rest = fr conversion other
      if (!rest.isEmpty) group(rest.repr)
     
    if (!r.isEmpty) group(r.repr)
    builder.result
   
 

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] = 
  new GroupIdenticalImpl(fr conversion r)

【讨论】:

以上是关于如何将丰富我的库模式应用于 Scala 集合?的主要内容,如果未能解决你的问题,请参考以下文章

再谈Scala集合

前端函数式攻城指南 pdf

如何绕过 Scala 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?

Scala:如何将模式匹配功能应用于包含以下值的 csv 文件

从零学scala集合模式匹配和样例类

Scala 集合如何从映射操作中返回正确的集合类型?