Ключом к пониманию этой проблемы является осознание того, что существует два разных способа создания коллекций в библиотеке коллекций и работы с ними . Один из них - это интерфейс публичных коллекций со всеми его хорошими методами. Другой, который широко используется при создании библиотеки коллекций, но почти никогда не используется вне ее, - это конструкторы.
Наша проблема в обогащении точно такая же, с которой сталкивается сама библиотека коллекций при попытке вернуть коллекции того же типа. То есть мы хотим создавать коллекции, но при общей работе у нас нет способа ссылаться на «тот же тип, которым уже является коллекция». Итак, нам нужны строители .
Теперь вопрос: откуда взять наших строителей? Очевидное место - из самой коллекции. Это не работает . При переходе к универсальной коллекции мы уже решили, что забудем о типе коллекции. Таким образом, даже несмотря на то, что коллекция может возвращать конструктор, который будет генерировать больше коллекций того типа, который нам нужен, он не будет знать, что это за тип.
Вместо этого мы получаем наших строителей из 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]
from C
- для большинства коллекций это будет просто идентификатор (например, 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]
для 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, !!)