Как применить шаблон обогащения-my-library к коллекциям Scala?

Один из самых мощных шаблонов, доступных в Scala, - это шаблон обогащения-my-library *, который использует неявные преобразования для добавления методов к существующим классам без необходимости разрешения динамического метода. Например, если бы мы пожелали, чтобы все строки имели метод spaces, который подсчитывал, сколько пробельных символов они имели, мы могли бы:

class SpaceCounter(s: String) {
  def spaces = s.count(_.isWhitespace)
}
implicit def string_counts_spaces(s: String) = new SpaceCounter(s)

scala> "How many spaces do I have?".spaces
res1: Int = 5

К сожалению, этот шаблон сталкивается с трудностями при работе с коллекциями. Например, задан ряд вопросов о группировании элементов последовательно с коллекциями. В одном снимке ничего не создано, поэтому это кажется идеальным кандидатом для шаблона обогащения my-library с использованием общей коллекции C и типа универсального элемента A:

class SequentiallyGroupingCollection[A, C[A] <: Seq[A]](ca: C[A]) {
  def groupIdentical: C[C[A]] = {
    if (ca.isEmpty) C.empty[C[A]]
    else {
      val first = ca.head
      val (same,rest) = ca.span(_ == first)
      same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
    }
  }
}

кроме, конечно, он не работает. REPL сообщает нам:

<console>:12: error: not found: value C
               if (ca.isEmpty) C.empty[C[A]]
                               ^
<console>:16: error: type mismatch;
 found   : Seq[Seq[A]]
 required: C[C[A]]
                 same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
                      ^

Есть две проблемы: как мы получаем C[C[A]] из пустого списка C[A] (или из воздуха)? И как мы получаем C[C[A]] обратно из строки same +: вместо Seq[Seq[A]]?

* Раньше была известна как сутенер-моя библиотека.

Ответ 1

Ключом к пониманию этой проблемы является осознание того, что существуют два разных способа создания и работы с коллекциями в библиотеке коллекций. Один из них - это интерфейс публичных коллекций со всеми его хорошими методами. Другие, которые широко используются при создании библиотеки коллекций, но которые почти никогда не используются за ее пределами, являются сборщиками.

Наша проблема в обогащении точно такая же, как и сама библиотека коллекций при попытке возврата коллекций того же типа. То есть мы хотим создавать коллекции, но при работе в общем случае у нас нет способа ссылаться на "тот же тип, что и в коллекции". Поэтому нам нужны строители.

Теперь возникает вопрос: откуда мы берем наших строителей? Очевидное место принадлежит самой коллекции. Это не работает. Мы уже решили, перейдя к общей коллекции, что мы собираемся забыть тип коллекции. Поэтому, даже если коллекция может вернуть построитель, который будет генерировать больше коллекций типа, который нам нужен, он не знал бы, что это за тип.

Вместо этого мы получаем наших сборщиков от CanBuildFrom implicits, которые плавают вокруг. Они существуют специально для сопоставления типов ввода и вывода и предоставления вам типично типизированного построителя.

Итак, у нас есть два концептуальных скачка:

  • Мы не используем стандартные операции с коллекциями, мы используем сборщики.
  • Мы получаем этих сборщиков из неявных 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 и строит C[A] s, а тот, который принимает C[A] и строит C[C[A]] s. Глядя на подпись типа CanBuildFrom, мы видим

CanBuildFrom[-From, -Elem, +To]

что означает, что CanBuildFrom хочет знать тип коллекции, с которой мы начинаем, - в нашем случае это C[A], а затем элементы сгенерированной коллекции и тип этой коллекции. Поэтому мы заполняем их как неявные параметры cbfcc и cbfc.

Понимая это, большая часть работы. Мы можем использовать наш CanBuildFrom, чтобы дать нам строителей (все, что вам нужно сделать, это применить их). И один строитель может создать коллекцию с помощью +=, преобразовать ее в коллекцию, которая, как предполагается, в конечном итоге будет с 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] уже является 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, !!)
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, !!)

Ответ 2

По сравнению с this commit намного легче "обогатить" коллекции Scala, чем это было, когда Рекс дал отличный ответ. Для простых случаев это может выглядеть так:

import scala.collection.generic.{ CanBuildFrom, FromRepr, HasElem }
import language.implicitConversions

class FilterMapImpl[A, Repr](val r : Repr)(implicit hasElem : HasElem[Repr, A]) {
  def filterMap[B, That](f : A => Option[B])
    (implicit cbf : CanBuildFrom[Repr, B, That]) : That = r.flatMap(f(_).toSeq)
}

implicit def filterMap[Repr : FromRepr](r : Repr) = new FilterMapImpl(r)

который добавляет "тот же тип результата" в отношении операции filterMap ко всем GenTraversableLike s,

scala> val l = List(1, 2, 3, 4, 5)
l: List[Int] = List(1, 2, 3, 4, 5)

scala> l.filterMap(i => if(i % 2 == 0) Some(i) else None)
res0: List[Int] = List(2, 4)

scala> val a = Array(1, 2, 3, 4, 5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a.filterMap(i => if(i % 2 == 0) Some(i) else None)
res1: Array[Int] = Array(2, 4)

scala> val s = "Hello World"
s: String = Hello World

scala> s.filterMap(c => if(c >= 'A' && c <= 'Z') Some(c) else None)
res2: String = HW

И для примера из вопроса, теперь решение выглядит,

class GroupIdenticalImpl[A, Repr : FromRepr](val r: Repr)
  (implicit hasElem : HasElem[Repr, A]) {
  def groupIdentical[That](implicit cbf: CanBuildFrom[Repr,Repr,That]): That = {
    val builder = cbf(r)
    def group(r: Repr) : Unit = {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if(!rest.isEmpty)
        group(rest)
    }
    if(!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[Repr : FromRepr](r: Repr) = new GroupIdenticalImpl(r)

Пример сеанса REPL,

scala> val l = List(1, 1, 2, 2, 3, 3, 1, 1)
l: List[Int] = List(1, 1, 2, 2, 3, 3, 1, 1)

scala> l.groupIdentical
res0: List[List[Int]] = List(List(1, 1),List(2, 2),List(3, 3),List(1, 1))

scala> val a = Array(1, 1, 2, 2, 3, 3, 1, 1)
a: Array[Int] = Array(1, 1, 2, 2, 3, 3, 1, 1)

scala> a.groupIdentical
res1: Array[Array[Int]] = Array(Array(1, 1),Array(2, 2),Array(3, 3),Array(1, 1))

scala> val s = "11223311"
s: String = 11223311

scala> s.groupIdentical
res2: scala.collection.immutable.IndexedSeq[String] = Vector(11, 22, 33, 11)

Снова отметим, что тот же принцип типа результата наблюдался точно так же, как если бы groupIdentical был определен непосредственно на GenTraversableLike.

Ответ 3

По сравнению с это фиксация магическое заклинание немного изменилось с того, что было, когда Майлз дал отличный ответ.

Следующие работы, но канонически? Надеюсь, один из канонов исправит это. (Вернее, пушки, одно из больших орудий.) Если граница обзора является верхней границей, вы теряете приложение в Array и String. Кажется, не имеет значения, является ли ограничение GenTraversableLike или TraversableLike; но IsTraversableLike дает вам GenTraversableLike.

import language.implicitConversions
import scala.collection.{ GenTraversable=>GT, GenTraversableLike=>GTL, TraversableLike=>TL }
import scala.collection.generic.{ CanBuildFrom=>CBF, IsTraversableLike=>ITL }

class GroupIdenticalImpl[A, R <% GTL[_,R]](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = {
    val builder = cbf(r.repr)
    def group(r: GTL[_,R]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[A, R <% GTL[_,R]](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

Есть более чем один способ кожи кошки с девятью жизнями. В этой версии говорится, что как только мой источник будет преобразован в GenTraversableLike, пока я могу построить результат из GenTraversable, просто сделайте это. Меня не интересует мой старый Реп.

class GroupIdenticalImpl[A, R](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[GT[A], GT[A], That]): That = {
    val builder = cbf(r.toTraversable)
    def group(r: GT[A]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r.toTraversable)
    builder.result
  }
}

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

Эта первая попытка включает уродливое преобразование Repr в GenTraversableLike.

import language.implicitConversions
import scala.collection.{ GenTraversableLike }
import scala.collection.generic.{ CanBuildFrom, IsTraversableLike }

type GT[A, B] = GenTraversableLike[A, B]
type CBF[A, B, C] = CanBuildFrom[A, B, C]
type ITL[A] = IsTraversableLike[A]

class FilterMapImpl[A, Repr](val r: GenTraversableLike[A, Repr]) { 
  def filterMap[B, That](f: A => Option[B])(implicit cbf : CanBuildFrom[Repr, B, That]): That = 
    r.flatMap(f(_).toSeq)
} 

implicit def filterMap[A, Repr](r: Repr)(implicit fr: ITL[Repr]): FilterMapImpl[fr.A, Repr] = 
  new FilterMapImpl(fr conversion r)

class GroupIdenticalImpl[A, R](val r: GT[A,R])(implicit fr: ITL[R]) { 
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = { 
    val builder = cbf(r.repr)
    def group(r0: R) { 
      val r = fr conversion r0
      val first = r.head
      val (same, other) = r.span(_ == first)
      builder += same
      val rest = fr conversion other
      if (!rest.isEmpty) group(rest.repr)
    } 
    if (!r.isEmpty) group(r.repr)
    builder.result
  } 
} 

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] = 
  new GroupIdenticalImpl(fr conversion r)