Как применить шаблон enrich-my-library к коллекциям Scala? - PullRequest
92 голосов
/ 23 марта 2011

Одним из наиболее мощных шаблонов, доступных в Scala, является шаблон enrich-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

К сожалению, этот шаблон сталкивается с проблемами при работе с универсальными коллекциями.Например, был задан ряд вопросов о группировании элементов последовательно с коллекциями .Нет ничего встроенного в том, что работает за один раз, так что это кажется идеальным кандидатом для шаблона enrich-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]]?

* Ранее известный как pimp-my-library.

Ответы [ 3 ]

74 голосов
/ 23 марта 2011

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

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

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

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

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

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

CanBuildFrom[-From, -Elem, +To]

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

Поняв это, это большая часть работы. Мы можем использовать наши CanBuildFrom, чтобы дать нам строителей (все, что вам нужно сделать, это применить их). И один сборщик может создать коллекцию с +=, преобразовать ее в коллекцию, с которой, как предполагается, в конечном итоге будет result, очистить себя и быть готовым начать заново с clear. Сборщики начинают с нуля, что решает нашу первую ошибку компиляции, и, поскольку мы используем сборщики вместо рекурсии, вторая ошибка также исчезает.

Еще одна маленькая деталь - кроме алгоритма, который фактически выполняет эту работу - в неявном преобразовании. Обратите внимание, что мы используем new GroupingCollection[A,C], а не [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, !!)
29 голосов
/ 18 мая 2012

Начиная с этого коммита"обогащать" коллекции 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.

9 голосов
/ 20 сентября 2012

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

Следующее работает, но канонично ли это? Я надеюсь, что один из канонов исправит это. (Вернее, пушки, одно из больших орудий.) Если граница вида - это верхняя граница, вы теряете приложение для 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)
...