Як застосувати шаблон збагачення моєї бібліотеки до колекцій Scala?


92

Одним з найпотужніших шаблонів, доступних у 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

На жаль, цей шаблон стикається з проблемами при роботі із загальними колекціями. Наприклад, було задано ряд питань щодо послідовного групування предметів за колекціями . Немає нічого вбудованого, що працює в один постріл, тому це здається ідеальним кандидатом для шаблону збагачення моєї бібліотеки із використанням загальної колекції 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 +:рядка замість a Seq[Seq[A]]?

* Раніше відомий як pimp-my-library.


1
Чудове питання! І, що ще краще, він має відповідь! :-)
Даніель К. Собрал,

2
@Daniel - Я не маю заперечень проти того, щоб це було з двома або більше відповідями!
Рекс Керр,

2
Забудь, друже. Я роблю закладки, щоб шукати їх, коли мені потрібно зробити щось подібне. :-)
Даніель К. Собрал

Відповіді:


74

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

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

Тепер питання: звідки ми беремо наших будівельників? Очевидне місце - від самої колекції. Це не працює . Переходячи до загальної колекції, ми вже вирішили забути тип колекції. Отже, навіть незважаючи на те, що колекція могла повернути конструктор, який би генерував більше колекцій того типу, який ми хочемо, він не знав би, який це тип.

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

Отже, у нас є два концептуальних стрибки:

  1. Ми не використовуємо стандартні операції колекцій, ми використовуємо конструктори.
  2. Ми отримуємо цих конструкторів з неявних CanBuildFroms, а не з нашої колекції безпосередньо.

Давайте розглянемо приклад.

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]]яка збирає всі групи разом. Отже, нам потрібні два будівельники, один, який бере As і будує C[A]s, і той, хто бере C[A]s і будує C[C[A]]s. Дивлячись на підпис типу CanBuildFrom, ми бачимо

CanBuildFrom[-From, -Elem, +To]

це означає, що CanBuildFrom хоче знати тип колекції, з якої ми починаємо - у нашому випадку це C[A], а потім елементи згенерованої колекції та тип цієї колекції. Отже, ми заповнюємо їх як неявні параметри cbfccта cbfc.

Зрозумівши це, це більша частина роботи. Ми можемо використовувати наші CanBuildFroms, щоб дати нам будівельників (все, що вам потрібно зробити, це застосувати їх). І один конструктор може створити колекцію +=, перетворити її на колекцію, з якою, в кінцевому рахунку, повинен бути 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, !!)

Ви можете використовувати <%, щоб додати підтримку для масивів.
Анонім

@Anonymous - Можна було б так підозрювати. Але чи ви пробували в цьому випадку?
Рекс Керр,

@Rex: "вимагати двох неявних перетворень поспіль" нагадує мені stackoverflow.com/questions/5332801/… Застосовується тут?
Петер Шмітц,

@Peter - Цілком можливо! Однак я схильний писати явні неявні перетворення, а не покладатися на ланцюжок <%.
Рекс Керр,

На основі коментаря @Peters я спробував додати ще одне неявне перетворення для масивів, але мені не вдалося. Я насправді не розумів, куди додати межі зору. @Rex, ти можеш редагувати свою відповідь і показати, як змусити код працювати з масивами?
kiritsuku

29

З цього коміту набагато легше "збагатити" колекції 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операції до всіх GenTraversableLikes,

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
Ага! Є ще більше чарівних творів, щоб відстежувати цей шлях, але всі вони чудово поєднуються! Це полегшення, якщо вам не потрібно турбуватися про кожну колекцію, яка не складається з ієрархії.
Рекс Керр,

3
Шкода, що Iterator безоплатно виключається, оскільки мою однорядкову зміну було відмовлено. "помилка: не вдалося знайти неявне значення параметра доказу типу scala.collection.generic.FromRepr [Ітератор [Int]]"
psp

У зміні якого одного рядка було відмовлено?
Майлз Сабін,


2
Я не бачу цього у господаря; він випарувався, чи опинився у гілці після 2.10.0, або ...?
Рекс Керр,

9

З цього моменту магічне заклинання трохи змінилося порівняно з тим, що було коли Майлз дав свою чудову відповідь.

Наступні роботи, але чи є це канонічним? Сподіваюся, хтось із канонів це виправить. (Вірніше, гармати, одна з великих гармат.) Якщо обмежена поданням є верхня межа, ви програєте програму 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.