【问题标题】:Scala Partition/Collect UsageScala 分区/收集用法
【发布时间】:2011-06-14 14:51:08
【问题描述】:

是否可以使用一次调用 collect 来创建 2 个新列表?如果没有,我该如何使用partition

【问题讨论】:

    标签: list scala collect


    【解决方案1】:

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

    这允许我们根据类型(collect 允许在分区列表中具有特定类型)或任何其他模式进行模式匹配:

    val (strings, ints) =
      List("a", 1, 2, "b", 19).partitionMap {
        case s: String => Left(s)
        case x: Int    => Right(x)
      }
    // strings: List[String] = List("a", "b")
    // ints: List[Int] = List(1, 2, 19)
    

    【讨论】:

      【解决方案2】:

      我个人会为此使用 foldLeft 或 foldRight。与此处的其他一些答案相比,它有几个优点。没有使用 var,所以这是一个纯函数(如果你关心那种类型的东西)。只有一次遍历列表。不会创建任何无关的 Either 对象。

      折叠的想法是将列表转换为单一类型。然而,没有什么能阻止我们让这个单一类型成为任意数量列表的元组。

      此示例将一个列表转换为三个不同的列表:

        val list: List[Any] = List(1,"two", 3, "four", 5.5)
      
        // Start with 3 empty lists and prepend to them each time we find a new value
        list.foldRight( (List.empty[Int]), List.empty[String], List.empty[Double]) {
          (nextItem, newCollection) => {
            nextItem match {
              case i: Int => newCollection.copy(_1 = i :: newCollection._1)
              case s: String => newCollection.copy(_2 = s :: newCollection._2)
              case f: Double => newCollection.copy(_3 = f :: newCollection._3)
              case _ => newCollection
            }
          }
        }
      

      【讨论】:

        【解决方案3】:

        这样的事情可能会有所帮助

        def partitionMap[IN, A, B](seq: Seq[IN])(function: IN => Either[A, B]): (Seq[A], Seq[B]) = {
          val (eitherLeft, eitherRight) = seq.map(function).partition(_.isLeft)
          eitherLeft.map(_.left.get) -> eitherRight.map(_.right.get)
        }
        

        叫它

        val seq: Seq[Any] = Seq(1, "A", 2, "B")
        val (ints, strings) = CollectionUtils.partitionMap(seq) {
          case int: Int    => Left(int)
          case str: String => Right(str)
        }
        ints shouldBe Seq(1, 2)
        strings shouldBe Seq("A", "B")
        

        Advantage 是一个简单的 API,类似于 Scala 2.12 中的 API

        缺点;集合运行了两次并且缺少对CanBuildFrom的支持

        【讨论】:

          【解决方案4】:

          我在这里找不到这个基本问题的令人满意的解决方案。 我不需要关于collect 的讲座,也不在乎这是否是某人的作业。另外,我不想要只对List 有效的东西。

          所以这是我的尝试。高效且兼容任何TraversableOnce,甚至是字符串:

          implicit class TraversableOnceHelper[A,Repr](private val repr: Repr)(implicit isTrav: Repr => TraversableOnce[A]) {
          
            def collectPartition[B,Left](pf: PartialFunction[A, B])
            (implicit bfLeft: CanBuildFrom[Repr, B, Left], bfRight: CanBuildFrom[Repr, A, Repr]): (Left, Repr) = {
              val left = bfLeft(repr)
              val right = bfRight(repr)
              val it = repr.toIterator
              while (it.hasNext) {
                val next = it.next
                if (!pf.runWith(left += _)(next)) right += next
              }
              left.result -> right.result
            }
          
            def mapSplit[B,C,Left,Right](f: A => Either[B,C])
            (implicit bfLeft: CanBuildFrom[Repr, B, Left], bfRight: CanBuildFrom[Repr, C, Right]): (Left, Right) = {
              val left = bfLeft(repr)
              val right = bfRight(repr)
              val it = repr.toIterator
              while (it.hasNext) {
                f(it.next) match {
                  case Left(next) => left += next
                  case Right(next) => right += next
                }
              }
              left.result -> right.result
            }
          }
          

          示例用法:

          val (syms, ints) =
            Seq(Left('ok), Right(42), Right(666), Left('ko), Right(-1)) mapSplit identity
          
          val ctx = Map('a -> 1, 'b -> 2) map {case(n,v) => n->(n,v)}
          val (bound, unbound) = Vector('a, 'a, 'c, 'b) collectPartition ctx
          println(bound: Vector[(Symbol, Int)], unbound: Vector[Symbol])
          

          【讨论】:

            【解决方案5】:

            不知道如何在不使用可变列表的情况下使用collect,但partition 也可以使用模式匹配(稍微详细一点)

            List("a", 1, 2, "b", 19).partition { 
              case s:String => true
              case _ => false 
            }
            

            【讨论】:

            • @coubeatczech - 因为分区返回(List[A],List[A])。这就是它所能做的所有事情,因为输入是List[A] 和指标函数A => Boolean。它无法知道指标函数可能是特定于类型的。
            • @Rex 我定义了我自己的collate 方法来解决这个问题的集合。在 List[A] 上,用例签名是 collate[B](fn: PartialFunction[A,B]): (List(B),List(A)),显然 actual 签名比那有点毛茸茸,因为我也在使用 CanBuildFrom
            【解决方案6】:

            我用这个。它的一个好处是它在一次迭代中结合了分区和映射。一个缺点是它确实分配了一堆临时对象(Either.LeftEither.Right 实例)

            /**
             * Splits the input list into a list of B's and a list of C's, depending on which type of value the mapper function returns.
             */
            def mapSplit[A,B,C](in: List[A])(mapper: (A) => Either[B,C]): (List[B], List[C]) = {
              @tailrec
              def mapSplit0(in: List[A], bs: List[B], cs: List[C]): (List[B], List[C]) = {
                in match {
                  case a :: as =>
                    mapper(a) match {
                      case Left(b)  => mapSplit0(as, b :: bs, cs     )
                      case Right(c) => mapSplit0(as, bs,      c :: cs)
                    }
                  case Nil =>
                    (bs.reverse, cs.reverse)
                }
              }
            
              mapSplit0(in, Nil, Nil)
            }
            
            val got = mapSplit(List(1,2,3,4,5)) {
              case x if x % 2 == 0 => Left(x)
              case y               => Right(y.toString * y)
            }
            
            assertEquals((List(2,4),List("1","333","55555")), got)
            

            【讨论】:

              【解决方案7】:

              collect 上常用的签名,比如Seq,是

              collect[B](pf: PartialFunction[A,B]): Seq[B]
              

              这确实是一个特例

              collect[B, That](pf: PartialFunction[A,B])(
                implicit bf: CanBuildFrom[Seq[A], B, That]
              ): That
              

              因此,如果您在默认模式下使用它,答案是否定的,当然不是:您会从中得到一个序列。如果您关注CanBuildFromBuilder,您会看到可以使That 实际上是两个序列,但是无法告诉项目应该进入哪个序列,因为部分函数可以只说“是的,我属于”或“不,我不属于”。

              那么,如果您想要有多个条件导致您的列表被分成一堆不同的部分,您会怎么做?一种方法是创建一个指标函数A => Int,其中您的A 被映射到一个编号的类,然后使用groupBy。例如:

              def optionClass(a: Any) = a match {
                case None => 0
                case Some(x) => 1
                case _ => 2
              }
              scala> List(None,3,Some(2),5,None).groupBy(optionClass)
              res11: scala.collection.immutable.Map[Int,List[Any]] = 
                Map((2,List(3, 5)), (1,List(Some(2))), (0,List(None, None)))
              

              现在您可以按类别(在本例中为 0、1 和 2)查找子列表。不幸的是,如果您想忽略某些输入,您仍然必须将它们放在一个类中(例如,在这种情况下,您可能不关心 None 的多个副本)。

              【讨论】:

                【解决方案8】:

                collect(在TraversableLike 上定义并在所有子类中可用)与集合和PartialFunction 一起使用。恰好在大括号内定义的一堆 case 子句是部分函数(参见Scala Language Specification 的第 8.5 节[警告 - PDF]

                和异常处理一样:

                try {
                  ... do something risky ...
                } catch {
                  //The contents of this catch block are a partial function
                  case e: IOException => ...
                  case e: OtherException => ...
                }
                

                这是定义一个只接受给定类型的某些值的函数的便捷方式。

                考虑在混合值列表中使用它:

                val mixedList = List("a", 1, 2, "b", 19, 42.0) //this is a List[Any]
                val results = mixedList collect {
                  case s: String => "String:" + s
                  case i: Int => "Int:" + i.toString
                }
                

                collect 方法的参数是 PartialFunction[Any,String]PartialFunction 因为它没有为Any 类型(即List 的类型)和String 的所有可能输入定义,因为这是所有子句返回的内容。

                如果您尝试使用map 而不是collect,则mixedList 末尾的双精度值将导致MatchError。使用 collect 只会丢弃它,以及任何其他未定义 PartialFunction 的值。

                一种可能的用途是将不同的逻辑应用于列表的元素:

                var strings = List.empty[String]
                var ints = List.empty[Int]
                mixedList collect {
                  case s: String => strings :+= s
                  case i: Int => ints :+= i
                }
                

                虽然这只是一个例子,但使用像这样的可变变量被许多人认为是战争罪 - 所以请不要这样做!

                很多更好的解决方案是使用 collect 两次:

                val strings = mixedList collect { case s: String => s }
                val ints = mixedList collect { case i: Int => i }
                

                或者,如果您确定列表只包含两种类型的值,您可以使用partition,它根据集合是否匹配某个谓词将集合拆分为值:

                //if the list only contains Strings and Ints:
                val (strings, ints) = mixedList partition { case s: String => true; case _ => false }
                

                这里的问题是stringsints 都是List[Any] 类型,尽管您可以轻松地将它们强制转换为更安全的类型(也许通过使用collect...)

                如果您已经有一个类型安全的集合,并且想要拆分元素的其他一些属性,那么事情对您来说会容易一些:

                val intList = List(2,7,9,1,6,5,8,2,4,6,2,9,8)
                val (big,small) = intList partition (_ > 5)
                //big and small are both now List[Int]s
                

                希望总结一下这两种方法对您的帮助!

                【讨论】:

                • 很好的解释,但我认为 OP 想要的是 collectpartition 的组合,它返回收集值列表的元组和所有其余值的列表。 def collectAndPartition[A, B](pf: PartialFunction[A, B]): (List[B], List[A])。这可能是使用本机库函数最优雅地实现的,即在 Traversable 的 collect 的源代码中,就像我们有 for (x <- this) if (pf.isDefinedAt(x)) b += pf(x) 一样,可以简单地在其末尾添加一个 else a += x,其中 a 将是其余所有列表的生成器。
                • 我知道 OP 需要什么,而且我也知道这是一个家庭作业问题(最近在堆栈溢出中提到了很多),所以我很乐意提供大量理论而不实际解决它。至于collectAndPartition,我已经写过了,虽然我将方法命名为collate。如果有人将 scala 的教学水平提高到学生应该使用 CanBuildFrom 的水平,那么我会感到非常惊讶,这超出了目前在生产中使用 scala 的大多数人。
                • 这很有帮助。但我仍然在想......是否有可能将正面和负面价值观分开,而不会像您之前写的那样构成“战争罪”?我只是好奇,因为我已经完成了使用分区的作业。哦...顺便说一句,感谢您的帮助!
                • collectmap 一样,获取一个集合并将其转换为另一个集合。它当然可以帮助处理数据并使其在后续操作中更容易分离,但是除非您依赖副作用(即可变变量),否则无法仅使用 collect 生成两个集合。这种 hack 会导致代码更难阅读,最好留到没有其他替代方案的时候使用,或者它可以显着提升性能,这两种方法都不适用。
                • this question 中有一个很好的例子,它展示了如何使用 collect 来拆分流。
                猜你喜欢
                • 2016-03-11
                • 1970-01-01
                • 2016-07-16
                • 1970-01-01
                • 2022-01-16
                • 1970-01-01
                • 2015-05-17
                • 2017-06-10
                • 2017-05-15
                相关资源
                最近更新 更多