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

Nor*_*dyk 17 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
Run Code Online (Sandbox Code Playgroud)

我一直在寻求实施一个隔离篮功能,对于给定的水果篮将返回单独的苹果和梨篮:

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])]
Run Code Online (Sandbox Code Playgroud)

这是我发现的最简洁的解决方案,但是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)
  }
Run Code Online (Sandbox Code Playgroud)

解决了额外水果类型的问题(扩展非常简单),但仍然强烈依赖于风险类型转换'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)
  }
Run Code Online (Sandbox Code Playgroud)

解决了显式转换的问题,但使用了可变集合,理想情况下我想坚持使用不可变集合和惯用代码.

我看过这里:Scala:根据类型过滤获得一些灵感,但也找不到更好的方法.

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

Rex*_*err 12

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

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


nli*_*lim 12

  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)
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)


Tra*_*own 10

使用Shapeless 2.0的LabelledGeneric类型类可以以非常干净和通用的方式完成此操作.首先,我们定义一个类型类,它将展示如何使用某些代数数据类型的元素将列表分区HList为每个构造函数的集合:

import shapeless._, record._

trait Partitioner[C <: Coproduct] extends DepFn1[List[C]] { type Out <: HList }
Run Code Online (Sandbox Code Playgroud)

然后是实例:

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 })
  }
}
Run Code Online (Sandbox Code Playgroud)

然后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))
Run Code Online (Sandbox Code Playgroud)

现在我们可以写下面的内容:

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

然后:

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))
Run Code Online (Sandbox Code Playgroud)

我们还可以编写一个版本来返回列表的元组而不是使用record('symbol)语法 - 有关详细信息,请参阅我的博客文章.

  • 这是什么巫术!我想要理解这一点非常糟糕(我正在写Scala以维持生活几年并且非常了解Scala,但这种类型的巫术与Scalaz/Shapeless/Algebraic类型/宏和那些`:+:`(联合类型? )我总是困扰着我并引起了我的兴趣.我一直对自己说,这是一种分心,我不需要它来做我的日常工作(而且我大部分时间都不需要),但我的自我没有'让我休息,直到我理解了所有这些.(我能理解`CanBuildFrom`并输入类!你会认为我会休息一下)我在哪里学到这一点? (5认同)

Xav*_*hot 8

Scala 2.13, Sets (和大多数集合)开始提供一种partitionMap方法,该方法根据返回Right或的函数对元素进行分区Left

通过对类型进行模式匹配,我们可以将PearsLeft[Pear]Apples映射到Right[Apple]forpartitionMap以创建梨和苹果的元组:

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))
Run Code Online (Sandbox Code Playgroud)