この問題を理解するための鍵は、コレクション ライブラリでコレクションを作成して操作する方法が 2 つあることを理解することです。1 つは、すべての優れたメソッドを備えたパブリック コレクション インターフェイスです。もう 1 つは、コレクション ライブラリの作成に広く使用されますが、それ以外ではほとんど使用されないビルダーです。
エンリッチメントの問題は、同じ型のコレクションを返そうとするときにコレクション ライブラリ自体が直面する問題とまったく同じです。つまり、コレクションを構築したいのですが、一般的に作業する場合、「コレクションが既に存在するのと同じ型」を参照する方法がありません。したがって、ビルダーが必要です。
問題は、ビルダーをどこから入手するかということです。明らかな場所は、コレクション自体からです。 これはうまくいきません。ジェネリック コレクションに移行する際に、コレクションの型を忘れることをすでに決めています。そのため、コレクションが必要な型のコレクションをさらに生成するビルダーを返すことができたとしても、その型が何であるかはわかりません。
CanBuildFrom
代わりに、あちこちにある暗黙のうちにビルダーを取得します。これらは特に、入力と出力の型を一致させ、適切に型指定されたビルダーを提供する目的で存在します。
そのため、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]
forC
とC[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, !!)