如何将“丰富我的图书馆”模式应用于Scala集合?


92

Scala中可用的最强大的模式之一是rich-my-library *模式,它使用隐式转换来显示将方法添加到现有类中,而无需动态方法解析。例如,如果我们希望所有字符串都具有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

不幸的是,这种模式在处理通用集合时会遇到麻烦。例如,有人问了一些有关按集合顺序对项目进行分组的问题。没有内置的功能可以一次性完成,因此这似乎是使用通用集合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[C[A]]从空C[A]列表(或凭空获得)获得a ?以及如何C[C[A]]same +:生产线获得回报而不是从生产线获得回报Seq[Seq[A]]

* 前身为皮条客我的图书馆。


1
好问题!而且,更好的是,它带有答案!:-)
Daniel C. Sobral

2
@Daniel-我不反对它有两个或多个答案!
Rex Kerr

2
算了,伙计。我将其添加为书签以在需要执行此类操作时进行查找。:-)
Daniel C. Sobral

Answers:


74

理解此问题的关键是要意识到,有两种不同的方法可以在集合库中构建和使用集合。一种是公共收藏夹接口及其所有好的方法。另一个是构建器,它广泛用于创建集合库,但几乎从未在其外部使用。

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

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

相反,我们从CanBuildFrom浮动的隐式对象中获取构建器。这些是专门为匹配输入和输出类型并为您提供适当类型的构建器而存在的。

因此,我们有两个概念上的飞跃:

  1. 我们没有使用标准的集合操作,而是在使用构建器。
  2. 我们从隐式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],然后是生成的集合的元素和该集合的类型。因此,我们将它们作为隐式参数cbfcc和填充cbfc

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

最后一个小细节-除了实际工作的算法以外-隐式转换中。请注意,我们使用new GroupingCollection[A,C]not [A,C[A]]。这是因为类声明是用于C一个参数的,它用A传递给它的自身填充。因此,我们只将其传递给type 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
  }
}

在这里,我们添加了一个隐式函数Iterable[A]C该隐式函数为我们提供了from-对于大多数集合来说,它只是身份(例如,List[A]已经是一个Iterable[A]),但是对于数组,它将是一个真正的隐式转换。而且,因此,我们放弃了这样的要求:C[A] <: Iterable[A]基本上,我们只是对<%显式进行了要求,因此我们可以随意显式使用它,而不必让编译器为我们填充它。此外,我们放宽了我们的收藏集的限制,C[C[A]]取而代之的是它是any 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] => 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 周围浮动了,因此可以从char集合中创建字符串。

让我们尝试一下:

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, !!)

您可以使用<%添加对数组的支持。
匿名

@匿名-人们会这样怀疑。但是您在这种情况下尝试过吗?
Rex Kerr

@Rex:“连续需要两次隐式转换”使我想起stackoverflow.com/questions/5332801/…适用于这里吗?
彼得·施米兹

@Peter-很可能!不过,我倾向于编写显式的隐式转换,而不是依靠<%链接。
Rex Kerr

基于@Peters注释,我尝试为数组添加另一个隐式转换,但是失败了。我不太了解在何处添加视图范围。@Rex,能否请您编辑答案并显示如何使代码与数组一起使用?
kiritsuku 2011年

29

截至此提交,“丰富” 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)

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

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)

同样,请注意,已经以与groupIdentical直接在上定义相同的方式观察到了相同的结果类型原理GenTraversableLike


3
好极了!还有更多神奇的片段可以跟踪这种方式,但是它们完美地结合在一起!不必担心每个非集合层次结构集合,这是一种放松。
雷克斯·克尔

3
由于拒绝我的单行更改,因此很遗憾地将Iterator排除在外。“错误:找不到类型为scala.collection.generic.FromRepr [Iterator [Int]]的证据参数的隐式值”
psp

拒绝单行更改?
Miles Sabin


2
我看不到主人 它蒸发了,还是结束于2.10.0之后的分支,或者...?
Rex Kerr

9

本次提交时,魔法咒语与Miles给出他的出色回答时的情况略有不同。

以下工作有效,但规范吗?我希望其中一个经典可以纠正它。(或者说,加农炮是其中的一门大炮。)如果视图范围是一个上限,则您将无法应用到Array和String。边界是GenTraversableLike还是TraversableLike似乎无关紧要;但是IsTraversableLike给您GenGenaversableLike。

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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.