Разбиение на основе типа в Scala

Учитывая следующую модель данных:

sealed trait Fruit

case class Apple(id: Int, sweetness: Int) extends Fruit

case class Pear(id: Int, color: String) extends Fruit

Я искал реализацию отдельной функции корзины, которая для данной корзины фруктов вернет отдельные корзины из яблок и груш:

def segregateBasket(fruitBasket: Set[Fruit]): (Set[Apple], Set[Pear])

Я попытался сделать несколько подходов, но ни один из них, похоже, не подгоняет счет. Ниже приведены мои попытки:

  def segregateBasket1(fruitBasket: Set[Fruit]): (Set[Apple], Set[Pear]) = fruitBasket
    .partition(_.isInstanceOf[Apple])
    .asInstanceOf[(Set[Apple], Set[Pear])]

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

  def segregateBasket2(fruitBasket: Set[Fruit]): (Set[Apple], Set[Pear]) = {
    val mappedFruits = fruitBasket.groupBy(_.getClass)
    val appleSet = mappedFruits.getOrElse(classOf[Apple], Set()).asInstanceOf[Set[Apple]]
    val pearSet = mappedFruits.getOrElse(classOf[Pear], Set()).asInstanceOf[Set[Pear]]
    (appleSet, pearSet)
  }

Решает проблему дополнительных типов фруктов (расширение очень простое), но все еще сильно зависит от рискованного типа "asInstanceOf", которого я бы предпочел избежать. Поэтому:

  def segregateBasket3(fruitBasket: Set[Fruit]): (Set[Apple], Set[Pear]) = {
    val appleSet = collection.mutable.Set[Apple]()
    val pearSet = collection.mutable.Set[Pear]()

    fruitBasket.foreach {
      case a: Apple => appleSet += a
      case p: Pear => pearSet += p
    }
    (appleSet.toSet, pearSet.toSet)
  }

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

Я смотрел здесь: Scala: Фильтрация по типу для какого-то вдохновения, но не могла найти лучшего подхода.

Есть ли какие-либо предложения о том, как эта функциональность может быть лучше реализована в Scala?

Ответ 1

  val emptyBaskets: (List[Apple], List[Pear]) = (Nil, Nil)

  def separate(fruits: List[Fruit]): (List[Apple], List[Pear]) = {
    fruits.foldRight(emptyBaskets) { case (f, (as, ps)) =>
      f match {
        case a @ Apple(_, _) => (a :: as, ps)
        case p @ Pear(_, _)  => (as, p :: ps)
      }
    }
  }

Ответ 2

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

def segregate4(basket: Set[Fruit]) = {
  val apples = basket.collect{ case a: Apple => a }
  val pears = basket.collect{ case p: Pear => p }
  (apples, pears)
}

Ответ 3

Это можно сделать очень простым и универсальным способом, используя класс типа Shapeless 2.0 LabelledGeneric. Сначала мы определяем класс типа, который покажет, как разбивать список с элементами некоторого типа алгебраических данных на HList для каждого конструктора:

import shapeless._, record._

trait Partitioner[C <: Coproduct] extends DepFn1[List[C]] { type Out <: HList }

И затем для экземпляров:

object Partitioner {
  type Aux[C <: Coproduct, Out0 <: HList] = Partitioner[C] { type Out = Out0 }

  implicit def cnilPartitioner: Aux[CNil, HNil] = new Partitioner[CNil] {
    type Out = HNil

    def apply(c: List[CNil]): Out = HNil
  }

  implicit def cpPartitioner[K, H, T <: Coproduct, OutT <: HList](implicit
    cp: Aux[T, OutT]
  ): Aux[FieldType[K, H] :+: T, FieldType[K, List[H]] :: OutT] =
    new Partitioner[FieldType[K, H] :+: T] {
      type Out = FieldType[K, List[H]] :: OutT

      def apply(c: List[FieldType[K, H] :+: T]): Out =
        field[K](c.collect { case Inl(h) => (h: H) }) ::
        cp(c.collect { case Inr(t) => t })
  }
}

И затем сам метод partition:

implicit def partition[A, C <: Coproduct, Out <: HList](as: List[A])(implicit
  gen: LabelledGeneric.Aux[A, C],
  partitioner: Partitioner.Aux[C, Out]
) = partitioner(as.map(gen.to))

Теперь мы можем написать следующее:

val fruits: List[Fruit] = List(
  Apple(1, 10),
  Pear(2, "red"),
  Pear(3, "green"),
  Apple(4, 6),
  Pear(5, "purple")
)

И затем:

scala> val baskets = partition(fruits)
partitioned: shapeless.:: ...

scala> baskets('Apple)
res0: List[Apple] = List(Apple(1,10), Apple(4,6))

scala> baskets('Pear)
res1: List[Pear] = List(Pear(2,red), Pear(3,green), Pear(5,purple))

Мы могли бы также написать версию, которая вернет кортеж списков вместо использования синтаксиса record('symbol) - см. мой пост в блоге здесь для деталей.

Ответ 4

Я немного смущен вашими примерами. Тип возврата каждого из ваших "сегрегатных" методов - это Tuple2, но вы хотите свободно добавлять больше типов Fruit. Ваш метод должен будет вернуть что-то с динамической длиной (Iterable/Seq/etc), так как длина кортежа должна быть детерминированной во время компиляции.

С учетом сказанного, возможно, я упрощаю это, но как насчет использования groupBy?

val fruit = Set(Apple(1, 1), Pear(1, "Green"), Apple(2, 2), Pear(2, "Yellow"))
val grouped = fruit.groupBy(_.getClass)

И затем сделайте все, что хотите, с помощью ключей/значений:

grouped.keys.map(_.getSimpleName).mkString(", ") //Apple, Pear
grouped.values.map(_.size).mkString(", ") //2, 2

ссылка: http://ideone.com/M4N0Pd

Ответ 5

Начиная Scala 2.13, Set (и большинство коллекций) снабжены Either[A1,A2]):(CC[A1],CC[A2]) rel="nofollow noreferrer"> partitionMap способом, который делит элементы на основе функции, которая возвращает либо Right или Left.

Путем сопоставления с образцом по типу мы можем отобразить Pear в Left[Pear] и Apple в Right[Apple] для partitionMap для создания кортежа груш и яблок:

val (pears, apples) =
  Set(Apple(1, 10), Pear(2, "red"), Apple(4, 6)).partitionMap {
    case pear: Pear   => Left(pear)
    case apple: Apple => Right(apple)
}
// pears: Set[Pear] = Set(Pear(2, "red"))
// apples: Set[Apple] = Set(Apple(1, 10), Apple(4, 6))