Ключем до розуміння цієї проблеми є усвідомлення того, що існує два різні способи побудови та роботи з колекціями в бібліотеці колекцій. Одним з них є інтерфейс публічних колекцій з усіма його приємними методами. Інший, який широко використовується при створенні бібліотеки колекцій, але який майже ніколи не використовується поза ним, - це будівельники.
Наша проблема збагачення - саме та, з якою стикається сама бібліотека колекцій при спробі повернути колекції того самого типу. Тобто ми хочемо створювати колекції, але, працюючи загально, ми не маємо можливості посилатися на "той самий тип, що колекція вже є". Тож нам потрібні будівельники .
Тепер питання: звідки ми беремо наших будівельників? Очевидне місце - від самої колекції. Це не працює . Переходячи до загальної колекції, ми вже вирішили забути тип колекції. Отже, навіть незважаючи на те, що колекція могла повернути конструктор, який би генерував більше колекцій того типу, який ми хочемо, він не знав би, який це тип.
Натомість ми отримуємо наших будівельників з CanBuildFrom
імпліцитів, які плавають навколо. Вони існують спеціально для того, щоб зіставити типи введення та виводу та дати вам відповідний тип конструктора.
Отже, у нас є два концептуальних стрибки:
- Ми не використовуємо стандартні операції колекцій, ми використовуємо конструктори.
- Ми отримуємо цих конструкторів з неявних
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]
для кожної групи, C[C[A]]
яка збирає всі групи разом. Отже, нам потрібні два будівельники, один, який бере A
s і будує C[A]
s, і той, хто бере C[A]
s і будує C[C[A]]
s. Дивлячись на підпис типу CanBuildFrom
, ми бачимо
CanBuildFrom[-From, -Elem, +To]
це означає, що CanBuildFrom хоче знати тип колекції, з якої ми починаємо - у нашому випадку це C[A]
, а потім елементи згенерованої колекції та тип цієї колекції. Отже, ми заповнюємо їх як неявні параметри cbfcc
та cbfc
.
Зрозумівши це, це більша частина роботи. Ми можемо використовувати наші CanBuildFrom
s, щоб дати нам будівельників (все, що вам потрібно зробити, це застосувати їх). І один конструктор може створити колекцію +=
, перетворити її на колекцію, з якою, в кінцевому рахунку, повинен бути result
, і спорожнити себе і бути готовим почати знову clear
. Конструктори починаються з порожнього, що вирішує нашу першу помилку компіляції, і оскільки ми використовуємо конструктори замість рекурсії, друга помилка також зникає.
Остання маленька деталь - крім алгоритму, який насправді виконує роботу - полягає в неявному перетворенні. Зверніть увагу, що ми використовуємо new GroupingCollection[A,C]
not [A,C[A]]
. Це пояснюється тим, що оголошення класу було 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))
Це працює!
Єдина проблема полягає в тому, що ми, як правило, не маємо цих методів, доступних для масивів, оскільки для цього потрібні дві неявні перетворення поспіль. Існує декілька способів обійти це, включаючи написання окремого неявного перетворення масивів, трансляцію 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]
з C
- для більшості колекцій це буде просто ідентичність (наприклад, List[A]
вже є an 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]
до C
і 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, !!)