92

Scalaで利用できる最も強力なパターンの1つは、enrich-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ワンショットで機能するものは組み込まれていないため、これはジェネリックコレクションとジェネリック要素タイプを使用したenrich-my-libraryパターンの理想的な候補のようです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
                      ^

2つの問題があります:C[C[A]]空のC[A]リスト(または薄い空気)からどのようにを取得しますか?そして、どうすればラインC[C[A]]から戻るのではsame +:なく、ラインから戻ることができSeq[Seq[A]]ますか?

*以前はpimp-my-libraryと呼ばれていました。

4

3 に答える 3

75

この問題を理解するための鍵は、コレクション ライブラリでコレクションを作成して操作する方法が 2 つあることを理解することです。1 つは、すべての優れたメソッドを備えたパブリック コレクション インターフェイスです。もう 1 つは、コレクション ライブラリの作成に広く使用されますが、それ以外ではほとんど使用されないビルダーです。

エンリッチメントの問題は、同じ型のコレクションを返そうとするときにコレクション ライブラリ自体が直面する問題とまったく同じです。つまり、コレクションを構築したいのですが、一般的に作業する場合、「コレクションが既に存在するのと同じ型」を参照する方法がありません。したがって、ビルダーが必要です。

問題は、ビルダーをどこから入手するかということです。明らかな場所は、コレクション自体からです。 これはうまくいきません。ジェネリック コレクションに移行する際に、コレクションの型を忘れることをすでに決めています。そのため、コレクションが必要な型のコレクションをさらに生成するビルダーを返すことができたとしても、その型が何であるかはわかりません。

CanBuildFrom代わりに、あちこちにある暗黙のうちにビルダーを取得します。これらは特に、入力と出力の型を一致させ、適切に型指定されたビルダーを提供する目的で存在します。

そのため、2 つの概念的な飛躍を行う必要があります。

  1. 標準のコレクション操作は使用していません。ビルダーを使用しています。
  2. これらのビルダーはCanBuildFrom、コレクションから直接ではなく、暗黙的な s から取得します。

例を見てみましょう。

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]まず、コレクションのコレクションを構築するには、グループごとに 2 種類のコレクションを構築する必要があることがわかっていますC[C[A]]。これにより、すべてのグループがまとめられます。したがって、2 つのビルダーが必要です。1 つは を受け取ってAをビルドし、もう 1C[A]つは をC[A]受け取って をビルドしC[C[A]]ます。の型シグネチャCanBuildFromを見ると、

CanBuildFrom[-From, -Elem, +To]

つまり、CanBuildFrom は、開始するコレクションの型を知りたいということです。この場合は でC[A]あり、生成されたコレクションの要素とそのコレクションの型です。したがって、これらを暗黙のパラメーターcbfccおよびとして入力しますcbfc

これに気づいたら、それがほとんどの作業です。s を使用CanBuildFromしてビルダーを提供できます (必要なのは、それらを適用することだけです)。そして、1 つのビルダーは、 を使用してコレクションを構築し、+=それを最終的に を使用するはずのコレクションに変換し、resultそれ自体を空にして、 を使用して再開する準備を整えることができclearます。ビルダーは空で開始するため、最初のコンパイル エラーは解決されます。再帰の代わりにビルダーを使用しているため、2 番目のエラーもなくなります。

実際に作業を行うアルゴリズム以外の最後の詳細は、暗黙的な変換にあります。new GroupingCollection[A,C]notを使用していることに注意してください[A,C[A]]。これは、クラス宣言が 1 つのパラメーターを使用するためのものであり、渡された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))

できます!

唯一の問題は、一般にこれらのメソッドを配列に使用できないことです。これは、連続して 2 つの暗黙的な変換が必要になるためです。これを回避するには、配列に対して別の暗黙的な変換を記述する、 にキャストするなど、いくつかの方法があります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]ここで、 fromを与える暗黙的なものを追加しましたC-- ほとんどのコレクションでは、これは単に ID になります (たとえば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]forCC[C[A]]forを接続し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
  )
}

文字列のベクトルの構築を処理するには new が必要ですCanBuildFrom(ただし、 を呼び出すだけなので、これは非常に簡単です)。次に、が適切に型付けVector.newBuilder[String]されるように、すべての型を入力する必要があります。GroupingCollectionすでに CanBuildFrom の周囲にあることに注意してください。その[String,Char,String]ため、文字列は文字のコレクションから作成できます。

試してみましょう:

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, !!)
于 2011-03-23T20:20:48.377 に答える
29

このコミットの時点で、Rexが優れた回答をしたときよりも、Scalaコレクションを「充実」させる方がはるかに簡単です。単純なケースでは、次のようになります。

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これは、すべてGenTraversableLikeのsに操作を尊重する「同じ結果タイプ」を追加します。

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

于 2012-05-18T09:46:44.230 に答える
9

このコミットの時点で、魔法の呪文は、Miles が優れた回答をしたときのものからわずかに変更されています。

以下は機能しますが、標準的ですか? カノンの1つがそれを修正することを願っています。(というか、大砲、大砲の 1 つです。) ビューの境界が上限である場合は、配列と文字列への適用が失われます。境界が 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)

9 命ある猫の皮を剥ぐ方法は 1 つではありません。このバージョンでは、ソースが GenTraversableLike に変換されたら、GenTraversable からの結果をビルドできる限り、それを実行するように指示されています。私は古いReprには興味がありません。

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)
于 2012-09-20T15:43:03.703 に答える