在Scala中基于类型的集合分区

时间:2014-06-13 16:45:42

标签: scala scala-collections shapeless

鉴于以下数据模型:

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: Filtering based on type获得了一些灵感,但也找不到更好的方法。

有没有人对如何在Scala中更好地实现此功能有任何建议?

5 个答案:

答案 0 :(得分:11)

“不可变”解决方案将使用您的可变解决方案,除非不显示集合。我不确定是否有充分的理由认为如果图书馆设计师这样做是可以的,但对你来说是诅咒。但是,如果你想坚持纯粹的不可变结构,这可能就像它得到的一样好:

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

答案 1 :(得分:10)

  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 :(得分:10)

使用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)语法的版本 - 请参阅my blog post here了解详细信息。

答案 3 :(得分:2)

我对你的例子感到有些困惑。每个&#34;隔离&#34;的返回类型方法是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

答案 4 :(得分:2)

Scala 2.13Set(和大多数集合)开始,提供了partitionMap方法,该方法根据返回Right或{{1 }}。

通过在类型上进行模式匹配,我们可以将Left的{​​{1}}映射到Pear并将Left[Pear]映射到Apple来创建梨和苹果:

Right[Apple]