この問題を理解するための鍵は、コレクションライブラリでコレクションを構築して操作するには2つの異なる方法があることを理解することです。1つは、すべての優れたメソッドを備えたパブリックコレクションインターフェイスです。もう1つは、コレクションライブラリの作成に広く使用されていますが、それ以外ではほとんど使用されないのはビルダーです。
エンリッチの問題は、同じタイプのコレクションを返そうとするときにコレクションライブラリ自体が直面する問題とまったく同じです。つまり、コレクションを構築したいのですが、一般的に作業する場合、「コレクションと同じ型」を参照する方法がありません。したがって、ビルダーが必要です。
さて、問題は、どこからビルダーを入手できるかです。明白な場所はコレクション自体からです。 これは動作しません。ジェネリックコレクションに移行する際に、コレクションの種類を忘れることは既に決定しました。したがって、コレクションが、必要なタイプのコレクションをさらに生成するビルダーを返すことができるとしても、タイプが何であるかはわかりません。
代わりに、CanBuildFrom
浮かんでいるインプリシットからビルダーを取得します。これらは特に、入力と出力の型を一致させ、適切に型指定されたビルダーを提供するために存在します。
したがって、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
。
これに気づいて、それはほとんどの仕事です。CanBuildFrom
sを使用してビルダーを提供できます(必要なのはそれらを適用することだけです)。そして、1つのビルダーは、を使用してコレクションを構築し、+=
それを最終的に使用するはずのコレクションに変換しresult
、それ自体を空にして、で再び開始する準備をすることができclear
ます。ビルダーは空から始まり、最初のコンパイルエラーを解決します。再帰の代わりにビルダーを使用しているため、2番目のエラーもなくなります。
最後に、実際に機能するアルゴリズム以外の詳細は、暗黙的な変換にあります。使用しnew GroupingCollection[A,C]
ないことに注意してください[A,C[A]]
。これは、クラス宣言がC
1つのパラメーターを使用して行わ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 C
と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
)
}
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, !!)