【问题标题】:Scala: Filtering based on typeScala:基于类型的过滤
【发布时间】:2011-04-04 02:03:22
【问题描述】:

我正在学习 Scala,因为它非常适合我的需求,但我发现很难优雅地构建代码。我的情况是,我有一个Listx,并且想要创建两个Lists:一个包含SomeClass 的所有元素,一个包含所有不属于SomeClass 的元素。

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

现在我的代码看起来像这样。但是,它不是很有效,因为它迭代了两次 x 并且代码似乎有点骇人听闻。有没有更好(更优雅)的做事方式?

可以假设SomeClass没有子类。

【问题讨论】:

    标签: class list scala functional-programming filter


    【解决方案1】:

    已编辑

    虽然可以使用普通的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 的回答。
    • 我明白为什么是List[Any],只是问题中使用的collect 将返回List[SomeClass],而分区会丢失此信息。
    • 如果你想让这个人“通用”,你还必须使用收藏库中的 CanBuildFrom 很棒。
    【解决方案2】:

    只是想用一个“更通用”的版本来扩展 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 -> 集合的“特定”类型(Vector、List、Seq 等)必须是更高种类的。我们可能可以解决一些类型推断问题(请参阅此处 Adrian 的回复:http://suereth.blogspot.com/2010/06/preserving-types-and-differing-subclass.html),但我感觉很懒;)

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

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

    最后,有趣的“CanBuildFrom”隐式参数使我们能够通用地构造特定类型,如 List 或 Vector。它们内置于所有核心库集合中。

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

    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...
    【解决方案3】:

    使用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 静态类型@987654326 @。因此,当您稍后在程序中使用 a 时,您将不得不再次检查运行时类型或无条件强制转换 [颤抖]。
    【解决方案4】:

    如果列表只包含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)
    

    【讨论】:

      【解决方案5】:

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

      这允许我们对给定类型(此处为Person)进行模式匹配,我们将其转换为Right,以便将其放入生成的分区元组的right 列表中。其他类型可以转化为Lefts在左侧进行分区:

      // 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))
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-05-17
        • 1970-01-01
        • 1970-01-01
        • 2011-01-14
        • 2018-05-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多