Scala:基于类型进行过滤

14

我正在学习Scala,因为它很适合我的需求,但我发现难以优雅地构建代码。 我遇到了这样一种情况:我有一个名为xList,想要创建两个List:一个包含SomeClass的所有元素,另一个包含不是SomeClass的所有元素。

val a = x collect {case y:SomeClass => y}
val b = x filterNot {_.isInstanceOf[SomeClass]}

目前我的代码看起来像这样。但是它并不是很高效,因为它两次迭代了x,而且代码似乎有点笨拙。有没有更好(更优雅)的方法来做同样的事情?

可以假设SomeClass没有子类。

5个回答

8

编辑过的内容

虽然使用普通的partition是可能的,但它失去了问题中collect保留的类型信息。

可以定义partition方法的一个变体,该方法接受返回两种类型之一的值的函数,使用Either实现:

import collection.mutable.ListBuffer

def partition[X,A,B](xs: List[X])(f: X=>Either[A,B]): (List[A],List[B]) = {
  val as = new ListBuffer[A]
  val bs = new ListBuffer[B]
  for (x <- xs) {
    f(x) match {
      case Left(a) => as += a
      case Right(b) => bs += b
    }
  }
  (as.toList, bs.toList)
}

然后保留类型:

scala> partition(List(1,"two", 3)) {
  case i: Int => Left(i)
  case x => Right(x)
}

res5: (List[Int], List[Any]) = (List(1, 3),List(two))

当然,使用构造器和所有改进的集合工具可以改进解决方案 :)。
为了完整起见,这是我的旧答案,使用普通的“partition”:
val (a,b) = x partition { _.isInstanceOf[SomeClass] }

例如:

scala> val x = List(1,2, "three")
x: List[Any] = List(1, 2, three)

scala> val (a,b) = x partition { _.isInstanceOf[Int] }
a: List[Any] = List(1, 2)
b: List[Any] = List(three)

那只是因为 x 是这样的。参见 @abhin4v 的回答。 - Alexey Romanov
1
我理解为什么要使用 List[Any],只是问题中使用的 collect 会返回一个 List[SomeClass],而 partition 会丢失这些信息。 - huynhjl
如果你想让这个家伙“通用”,你还需要在集合库中使用CanBuildFrom的强大功能。 - jsuereth

5

我想进一步扩展mkneissl的答案,提供一个“更通用”的版本,适用于库中的许多不同集合:

scala> import collection._
import collection._

scala> import generic.CanBuildFrom
import generic.CanBuildFrom

scala> def partition[X,A,B,CC[X] <: Traversable[X], To, To2](xs : CC[X])(f : X => Either[A,B])(
     |   implicit cbf1 : CanBuildFrom[CC[X],A,To], cbf2 : CanBuildFrom[CC[X],B,To2]) : (To, To2) = {
     |   val left = cbf1()
     |   val right = cbf2()
     |   xs.foreach(f(_).fold(left +=, right +=))
     |   (left.result(), right.result())
     | }
partition: [X,A,B,CC[X] <: Traversable[X],To,To2](xs: CC[X])(f: (X) => Either[A,B])(implicit cbf1: scala.collection.generic.CanBuildFrom[CC[X],A,To],implicit cbf2: scala.collection.generic.CanBuildFrom[CC[X],B,To2])(To, To2)

scala> partition(List(1,"two", 3)) {                                                                
     |   case i: Int => Left(i)                                                                     
     |   case x => Right(x)                                                                         
     | }
res5: (List[Int], List[Any]) = (List(1, 3),List(two))

scala> partition(Vector(1,"two", 3)) {
     |   case i: Int => Left(i)       
     |   case x => Right(x)           
     | }
res6: (scala.collection.immutable.Vector[Int], scala.collection.immutable.Vector[Any]) = (Vector(1, 3),Vector(two))

请注意:分区方法类似,但我们需要捕获几种类型:

X -> 集合中项目的原始类型。

A -> 左分区中项目的类型

B -> 右分区中项目的类型

CC -> 集合的“特定”类型(向量、列表、序列等)。这个必须是高级别的。我们可能可以解决一些类型推断问题(参见Adrian在这里的回答:http://suereth.blogspot.com/2010/06/preserving-types-and-differing-subclass.html),但我感到有点懒 ;)

To -> 左侧集合的完整类型

To2 -> 右侧集合的完整类型

最后,有趣的“CanBuildFrom”隐式参数允许我们以通用方式构建特定类型,如List或Vector。它们内置于所有核心库集合中。

具有讽刺意味的是,CanBuildFrom魔法的全部原因是正确处理BitSet。因为我要求CC是高级别的,所以当使用partition时,我们得到这个有趣的错误消息:

scala> partition(BitSet(1,2, 3)) {    
     |   case i if i % 2 == 0  => Left(i)
     |   case i if i % 2 == 1 => Right("ODD")
     | }
<console>:11: error: type mismatch;
 found   : scala.collection.BitSet
 required: ?CC[ ?X ]
Note that implicit conversions are not applicable because they are ambiguous:
 both method any2ArrowAssoc in object Predef of type [A](x: A)ArrowAssoc[A]
 and method any2Ensuring in object Predef of type [A](x: A)Ensuring[A]
 are possible conversion functions from scala.collection.BitSet to ?CC[ ?X ]
       partition(BitSet(1,2, 3)) {

如果需要的话,我会把这个问题留给别人来解决!我会试着给你提供一个在使用BitSet后仍然有效的解决方案。


太棒了,谢谢。我已经非常接近你的解决方案了,但没有想到将集合类型参数提高到更高级别。因此类型推断器推断出了“Nothing”... - mkneissl

4
使用list.partition
scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)

scala> val (even, odd) = l partition { _ % 2 == 0 }
even: List[Int] = List(2)
odd: List[Int] = List(1, 3)

编辑

对于按类型分区,请使用以下方法:

def partitionByType[X, A <: X](list: List[X], typ: Class[A]): 
    Pair[List[A], List[X]] = {
    val as = new ListBuffer[A]
    val notAs = new ListBuffer[X]
    list foreach {x =>
      if (typ.isAssignableFrom(x.asInstanceOf[AnyRef].getClass)) {
        as += typ cast x 
      } else {
        notAs += x
      }
    }
    (as.toList, notAs.toList)
}

使用方法:

scala> val (a, b) = partitionByType(List(1, 2, "three"), classOf[java.lang.Integer])
a: List[java.lang.Integer] = List(1, 2)
b: List[Any] = List(three)

虽然分区很酷,我最初也选择了这种方法,但在问题描述的情况下它并不适用,因为它没有给a静态类型List[SomeClass]。所以当你在程序中后面使用a时,你必须再次检查运行时类型或无条件地进行转换[发抖]。 - mkneissl

2
如果列表仅包含AnyRef的子类,则可以使用getClass方法来执行以下操作:
scala> case class Person(name: String)                                                           
defined class Person

scala> case class Pet(name: String)                                                              
defined class Pet

scala> val l: List[AnyRef] = List(Person("Walt"), Pet("Donald"), Person("Disney"), Pet("Mickey"))
l: List[AnyRef] = List(Person(Walt), Pet(Donald), Person(Disney), Pet(Mickey))

scala> val groupedByClass = l.groupBy(e => e.getClass)
groupedByClass: scala.collection.immutable.Map[java.lang.Class[_],List[AnyRef]] = Map((class Person,List(Person(Walt), Person(Disney))), (class Pet,List(Pet(Donald), Pet(Mickey))))

scala> groupedByClass(classOf[Pet])(0).asInstanceOf[Pet]
res19: Pet = Pet(Donald)

1

Scala 2.13开始,大多数集合现在都提供了partitionMap方法,该方法基于返回RightLeft的函数对元素进行分区。

这使我们能够模式匹配给定类型(这里是Person),将其作为Right转换并将其放置在结果分区元组的right列表中。其他类型可以被转换为Left以被分区到左侧:

// case class Person(name: String)
// case class Pet(name: String)
val (pets, persons) =
  List(Person("Walt"), Pet("Donald"), Person("Disney")).partitionMap {
    case person: Person => Right(person)
    case pet: Pet       => Left(pet)
  }
// persons: List[Person] = List(Person(Walt), Person(Disney))
// pets: List[Pet] = List(Pet(Donald))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接