Kunci untuk memahami masalah ini adalah dengan menyadari bahwa ada dua cara berbeda untuk membangun dan bekerja dengan koleksi di perpustakaan koleksi. Salah satunya adalah antarmuka koleksi publik dengan semua metodenya yang bagus. Yang lainnya, yang digunakan secara ekstensif dalam membuat perpustakaan koleksi, tetapi yang hampir tidak pernah digunakan di luarnya, adalah pembangun.
Masalah kita dalam memperkaya sama persis dengan yang dihadapi perpustakaan koleksi itu sendiri ketika mencoba mengembalikan koleksi dari jenis yang sama. Artinya, kami ingin membangun koleksi, tetapi saat bekerja secara umum, kami tidak memiliki cara untuk merujuk ke "tipe yang sama dengan koleksi tersebut". Jadi kami membutuhkan pembangun .
Sekarang pertanyaannya adalah: dari mana kita mendapatkan pembangun kita? Tempat yang jelas adalah dari koleksi itu sendiri. Ini tidak berhasil . Kami telah memutuskan, saat pindah ke koleksi umum, bahwa kami akan melupakan jenis koleksinya. Jadi, meskipun collection dapat mengembalikan pembangun yang akan menghasilkan lebih banyak koleksi dari jenis yang kita inginkan, ia tidak akan tahu apa jenisnya.
Sebagai gantinya, kita mendapatkan pembangun kita dari CanBuildFrom
implikasinya yang beredar. Ini ada secara khusus untuk tujuan mencocokkan jenis masukan dan keluaran dan memberi Anda pembangun yang diketik dengan tepat.
Jadi, kami memiliki dua lompatan konseptual yang harus dilakukan:
- Kami tidak menggunakan operasi pengumpulan standar, kami menggunakan pembangun.
- Kami mendapatkan pembangun ini dari implisit
CanBuildFrom
, bukan dari koleksi kami secara langsung.
Mari kita lihat contohnya.
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)
}
Mari kita pisahkan ini. Pertama, untuk membangun koleksi-koleksi, kita tahu kita perlu membangun dua jenis koleksi: C[A]
untuk setiap grup, dan C[C[A]]
itu mengumpulkan semua grup bersama-sama. Jadi, kita membutuhkan dua pembangun, satu yang mengambil A
s dan membangun C[A]
, dan satu yang mengambil C[A]
s dan membangun C[C[A]]
. Melihat jenis tanda tangan CanBuildFrom
, kita lihat
CanBuildFrom[-From, -Elem, +To]
yang berarti CanBuildFrom ingin mengetahui jenis koleksi yang kita mulai - dalam kasus kita, ini C[A]
, lalu elemen koleksi yang dihasilkan dan jenis koleksi itu. Jadi kami mengisinya sebagai parameter implisit cbfcc
dan cbfc
.
Setelah menyadari ini, itulah sebagian besar pekerjaan. Kami dapat menggunakan milik kami CanBuildFrom
untuk memberi kami pembangun (yang perlu Anda lakukan hanyalah menerapkannya). Dan satu pembangun dapat membuat koleksi dengan +=
, mengubahnya menjadi koleksi yang seharusnya ada result
, dan mengosongkan dirinya sendiri serta siap untuk memulai lagi clear
. Pembangun mulai dari kosong, yang memecahkan kesalahan kompilasi pertama kita, dan karena kita menggunakan pembangun alih-alih rekursi, kesalahan kedua juga akan hilang.
Satu detail kecil terakhir - selain algoritme yang benar-benar berfungsi - ada dalam konversi implisit. Perhatikan bahwa kami new GroupingCollection[A,C]
tidak menggunakan [A,C[A]]
. Ini karena deklarasi kelas adalah untuk C
dengan satu parameter, yang akan mengisinya sendiri dengan yang A
diteruskan padanya. Jadi kita hanya menyerahkan jenisnya C
, dan membiarkannya C[A]
membuatnya. Detail kecil, tetapi Anda akan mendapatkan kesalahan waktu kompilasi jika Anda mencoba cara lain.
Di sini, saya telah membuat metode ini sedikit lebih umum daripada kumpulan "elemen yang sama" - alih-alih, metode ini memotong koleksi asli setiap kali pengujiannya terhadap elemen sekuensial gagal.
Mari kita lihat metode kita beraksi:
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))
Berhasil!
Satu-satunya masalah adalah bahwa kita secara umum tidak memiliki metode ini tersedia untuk array, karena itu akan membutuhkan dua konversi implisit berturut-turut. Ada beberapa cara untuk menyiasatinya, termasuk menulis konversi implisit terpisah untuk array, mentransmisikan ke WrappedArray
, dan sebagainya.
Sunting: Pendekatan favorit saya untuk menangani array dan string dan semacamnya adalah membuat kode menjadi lebih umum dan kemudian menggunakan konversi implisit yang sesuai untuk membuatnya lebih spesifik lagi sedemikian rupa sehingga array juga berfungsi. Dalam kasus khusus ini:
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
}
}
Di sini kita telah menambahkan implisit yang memberi kita Iterable[A]
dari C
--untuk sebagian besar koleksi ini hanya akan menjadi identitas (misalnya List[A]
sudah merupakan Iterable[A]
), tetapi untuk array itu akan menjadi konversi implisit nyata. Dan, akibatnya, kami telah menghilangkan persyaratan bahwa - C[A] <: Iterable[A]
kami pada dasarnya baru saja membuat persyaratan untuk <%
eksplisit, jadi kami dapat menggunakannya secara eksplisit sesuka hati alih-alih meminta kompiler mengisinya untuk kami. Juga, kami telah melonggarkan batasan bahwa koleksi-koleksi kami adalah C[C[A]]
--malahan, apa saja D[C]
, yang akan kami isi nanti untuk menjadi apa yang kami inginkan. Karena kita akan mengisinya nanti, kita telah mendorongnya ke tingkat kelas alih-alih ke tingkat metode. Kalau tidak, pada dasarnya sama.
Sekarang pertanyaannya adalah bagaimana menggunakan ini. Untuk koleksi biasa, kami dapat:
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)
}
di mana sekarang kita pasang C[A]
untuk C
dan C[C[A]]
untuk D[C]
. Perhatikan bahwa kita memang membutuhkan tipe generik eksplisit pada panggilan tersebut new GroupingCollection
sehingga dapat menjaga tipe mana yang sesuai dengan apa. Berkat implicit c2i: C[A] => Iterable[A]
, ini secara otomatis menangani array.
Tapi tunggu dulu, bagaimana jika kita ingin menggunakan string? Sekarang kami dalam masalah, karena Anda tidak dapat memiliki "string". Di sinilah abstraksi ekstra membantu: kita dapat memanggil D
sesuatu yang cocok untuk menyimpan string. Ayo pilih Vector
, dan lakukan hal berikut:
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
)
}
Kita membutuhkan yang baru CanBuildFrom
untuk menangani pembangunan vektor string (tapi ini sangat mudah, karena kita hanya perlu memanggil Vector.newBuilder[String]
), dan kemudian kita perlu mengisi semua tipe sehingga GroupingCollection
diketik dengan bijaksana. Perhatikan bahwa kita telah mengapung di sekitar [String,Char,String]
CanBuildFrom, sehingga string dapat dibuat dari kumpulan karakter.
Mari kita coba:
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, !!)