Разбиение на основе типа в 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))