Функциональные шаблоны для лучшей цепочки сбора - PullRequest
5 голосов
/ 06 августа 2020

Мне часто нужно объединить collects в цепочку, где я хочу выполнить несколько сборов за один проход. Я также хотел бы вернуть «остаток» для вещей, которые не соответствуют ни одной из коллекций.

Например:

sealed trait Animal
case class Cat(name: String) extends Animal
case class Dog(name: String, age: Int) extends Animal

val animals: List[Animal] =
  List(Cat("Bob"), Dog("Spot", 3), Cat("Sally"), Dog("Jim", 11))

// Normal way
val cats: List[Cat]    = animals.collect { case c: Cat => c }
val dogAges: List[Int] = animals.collect { case Dog(_, age) => age }
val rem: List[Animal]  = Nil // No easy way to create this without repeated code

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

Вместо этого я обычно выбираю мутацию, которая очень похожа на logi c, которую вы бы иметь в fold:

import scala.collection.mutable.ListBuffer

// Ugly, hide the mutation away
val (cats2, dogsAges2, rem2) = {
  // Lose some benefits of type inference
  val cs = ListBuffer[Cat]()
  val da = ListBuffer[Int]()
  val rem = ListBuffer[Animal]()
  // Bad separation of concerns, I have to merge all of my functions
  animals.foreach {
    case c: Cat      => cs += c
    case Dog(_, age) => da += age
    case other       => rem += other
  }
  (cs.toList, da.toList, rem.toList)
}

Мне не нравится этот бит, у него хуже вывод типов и разделение проблем, так как мне нужно объединить все различные частичные функции. Это также требует большого количества строк кода.

Я хочу несколько полезных шаблонов, таких как collect, который возвращает остаток (я допускаю, что partitionMap новое в 2.13 делает это, но уродливее). Я также мог бы использовать некоторую форму pipe или map для работы с частями кортежей. Вот несколько надуманных утилит:

implicit class ListSyntax[A](xs: List[A]) {
  import scala.collection.mutable.ListBuffer
  // Collect and return remainder
  // A specialized form of new 2.13 partitionMap
  def collectR[B](pf: PartialFunction[A, B]): (List[B], List[A]) = {
    val rem = new ListBuffer[A]()
    val res = new ListBuffer[B]()
    val f = pf.lift
    for (elt <- xs) {
      f(elt) match {
        case Some(r) => res += r
        case None    => rem += elt
      }
    }
    (res.toList, rem.toList)
  }
}
implicit class Tuple2Syntax[A, B](x: Tuple2[A, B]){
  def chainR[C](f: B => C): Tuple2[A, C] = x.copy(_2 = f(x._2))
}

Теперь я могу написать это так, чтобы можно было выполнить за один проход (с ленивой структурой данных) и при этом следовать функциональности, неизменная практика:

// Relatively pretty, can imagine lazy forms using a single iteration
val (cats3, (dogAges3, rem3)) =
  animals.collectR          { case c: Cat => c }
         .chainR(_.collectR { case Dog(_, age) => age })

Мой вопрос: есть ли такие шаблоны? Пахнет чем-то вроде того, что было бы в библиотеке, такой как Cats, FS2 или ZIO, но я не уверен, как это можно назвать.

Scast ie ссылка на примеры кода: https://scastie.scala-lang.org/Egz78fnGR6KyqlUTNTv9DQ

Ответы [ 4 ]

7 голосов
/ 06 августа 2020

Я хотел увидеть, насколько «противным» будет fold().

val (cats
    ,dogAges
    ,rem) = animals.foldRight((List.empty[Cat]
                              ,List.empty[Int]
                              ,List.empty[Animal])) {
  case (c:Cat,   (cs,ds,rs)) => (c::cs, ds, rs)
  case (Dog(_,d),(cs,ds,rs)) => (cs, d::ds, rs)
  case (r,       (cs,ds,rs)) => (cs, ds, r::rs)
}

Глаз смотрящего, я полагаю.

4 голосов
/ 06 августа 2020

Как насчет определения пары служебных классов, которые помогут вам в этом?

case class ListCollect[A](list: List[A]) {
  def partialCollect[B](f: PartialFunction[A, B]): ChainCollect[List[B], A] = {
    val (cs, rem) = list.partition(f.isDefinedAt)
    new ChainCollect((cs.map(f), rem))
  }
}

case class ChainCollect[A, B](tuple: (A, List[B])) {
  def partialCollect[C](f: PartialFunction[B, C]): ChainCollect[(A, List[C]), B] = {
    val (cs, rem) = tuple._2.partition(f.isDefinedAt)
    ChainCollect(((tuple._1, cs.map(f)), rem))
  }
}

ListCollect предназначено только для начала цепочки, а ChainCollect берет предыдущий остаток (второй элемент кортеж) и пытается применить к нему PartialFunction, создавая новый объект ChainCollect. Мне не особенно нравятся вложенные кортежи, которые это создает, но вы можете сделать их немного лучше, если используете Shapeless HList s.

val ((cats, dogs), rem) = ListCollect(animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .tuple

Scast ie

Тип *: Дотти делает это немного проще:

opaque type ChainResult[Prev <: Tuple, Rem] = (Prev, List[Rem])

extension [P <: Tuple, R, N](chainRes: ChainResult[P, R]) {
  def partialCollect(f: PartialFunction[R, N]): ChainResult[List[N] *: P, R] = {
    val (cs, rem) = chainRes._2.partition(f.isDefinedAt)
    (cs.map(f) *: chainRes._1, rem)
  }
}

Это приводит к переворачиванию вывода, но у него нет такой уродливой вложенности, как в моем предыдущем подходе:


val ((owls, dogs, cats), rem) = (EmptyTuple, animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .partialCollect { case Owl(wisdom) => wisdom }

/* more animals */

case class Owl(wisdom: Double) extends Animal
case class Fly(isAnimal: Boolean) extends Animal

val animals: List[Animal] =
  List(Cat("Bob"), Dog("Spot", 3), Cat("Sally"), Dog("Jim", 11), Owl(200), Fly(false))

Scast ie

И если вам это все еще не нравится, вы всегда можете определить еще несколько вспомогательных методов для изменения кортежа, добавив расширение в Список, не требующий для начала EmptyTuple, et c.

//Add this to the ChainResult extension
def end: Reverse[List[R] *: P] = {
    def revHelp[A <: Tuple, R <: Tuple](acc: A, rest: R): RevHelp[A, R] =
      rest match {
        case EmptyTuple => acc.asInstanceOf[RevHelp[A, R]]
        case h *: t => revHelp(h *: acc, t).asInstanceOf[RevHelp[A, R]]
      }
    revHelp(EmptyTuple, chainRes._2 *: chainRes._1)
  }

//Helpful types for safety
type Reverse[T <: Tuple] = RevHelp[EmptyTuple, T]
type RevHelp[A <: Tuple, R <: Tuple] <: Tuple = R match {
  case EmptyTuple => A
  case h *: t => RevHelp[h *: A, t]
}

И теперь вы можете сделать это:

val (cats, dogs, owls, rem) = (EmptyTuple, animals)
  .partialCollect { case c: Cat => c }
  .partialCollect { case Dog(_, age) => age }
  .partialCollect { case Owl(wisdom) => wisdom }
  .end

Scast ie

2 голосов
/ 06 августа 2020

Поскольку вы упомянули кошек, я бы также добавил решение, используя foldMap:

sealed trait Animal
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
case class Snake(name: String) extends Animal

val animals: List[Animal] = List(Cat("Bob"), Dog("Spot"), Cat("Sally"), Dog("Jim"), Snake("Billy"))

val map = animals.foldMap{ //Map(other -> List(Snake(Billy)), cats -> List(Cat(Bob), Cat(Sally)), dogs -> List(Dog(Spot), Dog(Jim)))
  case d: Dog => Map("dogs" -> List(d))
  case c: Cat => Map("cats" -> List(c))
  case o => Map("other" -> List(o))
}

val tuples = animals.foldMap{ //(List(Dog(Spot), Dog(Jim)),List(Cat(Bob), Cat(Sally)),List(Snake(Billy)))
  case d: Dog => (List(d), Nil, Nil)
  case c: Cat => (Nil, List(c), Nil)
  case o => (Nil, Nil, List(o))
}

Возможно, это более лаконично, чем версия сгиба, но она должна объединять частичные результаты с использованием моноидов, поэтому она не будет быть таким же эффективным.

2 голосов
/ 06 августа 2020

Этот код делит список на три набора, поэтому естественный способ сделать это - использовать partition дважды:

val (cats, notCat) = animals.partitionMap{
  case c: Cat => Left(c)
  case x => Right(x)
}

val (dogAges, rem) = notCat.partitionMap {
  case Dog(_, age) => Left(age)
  case x => Right(x)
}

Вспомогательный метод может упростить это

def partitionCollect[T, U](list: List[T])(pf: PartialFunction[T, U]): (List[U], List[T]) =
  list.partitionMap {
    case t if pf.isDefinedAt(t) => Left(pf(t))
    case x => Right(x)
  }

val (cats, notCat) = partitionCollect(animals) { case c: Cat => c }
val (dogAges, rem) = partitionCollect(notCat) { case Dog(_, age) => age }

Это явно расширяемо для большего количества категорий, с небольшим раздражением придумывать имена временных переменных (что можно было бы преодолеть с помощью явных n-сторонних методов разделения)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...