ライブラリーの充実パターンをScalaコレクションに適用するにはどうすればよいですか?


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とジェネリックエレメントタイプを使用したrich-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と呼ばれていました。


1
すばらしい質問です。そして、さらに良いことに、それは答えが付属しています!:-)
ダニエルCソブラル

2
@Daniel-2つ以上の回答があるので、それに異議はありません。
Rex Kerr

2
忘れて、バディ。これをブックマークして、このようなことをする必要があるときはいつでも調べます。:-)
ダニエルCソブラル

回答:


74

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

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

さて、問題は、どこからビルダーを入手できるかです。明白な場所はコレクション自体からです。 これは動作しません。ジェネリックコレクションに移行する際に、コレクションの種類を忘れることは既に決定しました。したがって、コレクションが、必要なタイプのコレクションをさらに生成するビルダーを返すことができるとしても、タイプが何であるかはわかりません。

代わりに、CanBuildFrom浮かんでいるインプリシットからビルダーを取得します。これらは特に、入力と出力の型を一致させ、適切に型指定されたビルダーを提供するために存在します。

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

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

例を見てみましょう。

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)
}

これを分解してみましょう。最初に、コレクションのコレクションを構築するために、2つのタイプのコレクションを構築する必要があることを知っています:C[A]各グループ用で、C[C[A]]すべてのグループをまとめます。このように、我々は2つのビルダー、かかるいずれかが必要ですA秒と構築するC[A]のを、そして取る1 C[A]秒と構築するC[C[A]]のを。の型シグネチャCanBuildFromを見ると、

CanBuildFrom[-From, -Elem, +To]

つまり、CanBuildFromは、開始するコレクションのタイプ(この場合は)を知り、C[A]次に、生成されたコレクションの要素とそのコレクションのタイプを知りたいということです。だから我々は暗黙のパラメータとしてのものを記入cbfccしてcbfc

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

最後に、実際に機能するアルゴリズム以外の詳細は、暗黙的な変換にあります。使用しnew GroupingCollection[A,C]ないことに注意してください[A,C[A]]。これは、クラス宣言がC1つのパラメーターを使用して行わAれたためです。したがってC、それをtype に渡し、それから作成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]]代わりに、それは必要な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]for CC[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
  )
}

CanBuildFrom文字列のベクトルの構築を処理するためのnew が必要です(ただし、を呼び出すだけなので、これは非常に簡単です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, !!)

<%を使用して、配列のサポートを追加できます。
匿名の

@匿名-疑わしいだろう。しかし、あなたはこの場合それを試しましたか?
レックスカー

@Rex:「続けて2つの暗黙的な変換が必要」と言うと、stackoverflow.com / questions / 5332801 /を思い出させます… ここで適用できますか?
Peter Schmitz、2011年

@ピーター-かなり可能性があります!ただし、<%チェーニングに依存するのではなく、明示的な暗黙の変換を記述する傾向があります。
Rex Kerr

@Petersのコメントに基づいて、配列に別の暗黙の変換を追加しようとしましたが、失敗しました。ビューの境界を追加する場所がよくわかりませんでした。@Rex、答えを編集して、コードを配列で機能させる方法を教えてください。
キリツク

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

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
私の1行の変更が拒否されたので、イタレータは残念ながら除外されました。「エラー:タイプscala.collection.generic.FromRepr [Iterator [Int]]のエビデンスパラメータの暗黙的な値が見つかりませんでした」
psp

どの1行の変更が拒否されましたか?
Miles Sabin


2
私はこれをマスターには見ません。それは蒸発しましたか、それとも2.10.0以降のブランチになりましたか、それとも...?
Rex Kerr

9

このコミットの時点で、魔法の呪文はマイルスが素晴らしい答えを出したときのものとは少し異なります。

以下は機能しますが、それは正規ですか?カノンの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つの命を与えるには、複数の方法があります。このバージョンでは、ソースが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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.