web-dev-qa-db-ja.com

enrich-my-libraryパターンをScalaコレクションに適用するにはどうすればよいですか?

Scalaで利用可能な最も強力なパターンの1つは、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

残念ながら、このパターンは、ジェネリックコレクションを処理するときに問題が発生します。たとえば、 アイテムをコレクションで順番にグループ化する について多くの質問がありました。ワンショットで機能するものは組み込まれていないため、これは、ジェネリックコレクションCとジェネリック要素タイプAを使用したenrich-my-libraryパターンの理想的な候補のようです。

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として知られていました。

92
Rex Kerr

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

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

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

代わりに、浮かんでいるCanBuildFrom暗黙からビルダーを取得します。これらは、特に入力タイプと出力タイプを照合し、適切にタイプされたビルダーを提供する目的で存在します。

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

  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]]の2種類のコレクションを作成する必要があることがわかっています。したがって、2つのビルダーが必要です。1つはAsを取得してC[A]sをビルドし、もう1つはC[A]sを取得してC[C[A]]sをビルドします。 CanBuildFromの型シグネチャを見ると、

CanBuildFrom[-From, -Elem, +To]

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

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

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

できます!

唯一の問題は、これらのメソッドを配列に使用できないことです。これは、2つの暗黙的な変換を連続して行う必要があるためです。これを回避するには、配列の個別の暗黙的な変換を作成する、WrappedArrayにキャストするなど、いくつかの方法があります。


編集:配列や文字列などを処理するための私の好ましいアプローチは、コードをさらにmore汎用にし、適切な暗黙の変換を使用して、配列も機能するようにそれらをより具体的にすることです。この特定のケースでは:

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

ここに、CからIterable[A]を与える暗黙的なものを追加しました。ほとんどのコレクションでは、これは単なる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の場合はC[A]を、C[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の周りに浮かんでいるので、文字のコレクションから文字列を作成できることに注意してください。

それを試してみましょう:

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, !!)
74
Rex Kerr

このコミット の時点で、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操作に関する「同じ結果タイプ」がすべての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)

繰り返しになりますが、同じ結果タイプの原則が、groupIdenticalGenTraversableLikeで直接定義された場合とまったく同じ方法で観察されていることに注意してください。

29
Miles Sabin

このコミット 現在、魔法の呪文はマイルズが彼の優れた答えを出したときのものからわずかに変更されています。

以下は動作しますが、それは標準的ですか?カノンの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)
9
som-snytt