38

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

4

8 回答 8

81

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

与异常处理一样:

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
}

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

如果您尝试使用map而不是collect,则末尾的双精度值mixedList将导致MatchError. 使用collectjust 会丢弃此值以及未定义 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都是 type 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

希望总结一下这两种方法如何在这里对您有所帮助!

于 2011-01-24T16:44:49.133 回答
7

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

List("a", 1, 2, "b", 19).partition { 
  case s:String => true
  case _ => false 
}
于 2011-01-24T16:12:06.890 回答
6

通常使用的签名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

因此,如果您在默认模式下使用它,答案是否定的,当然不是:您会从中得到一个序列。如果你CanBuildFrom通过Builder,你会发现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不幸的是,如果您想忽略某些输入,您仍然必须将它们放在一个类中(例如,在这种情况下您可能不关心多个副本)。

于 2011-01-24T17:03:20.667 回答
5

我用这个。它的一个好处是它在一次迭代中结合了分区和映射。一个缺点是它确实分配了一堆临时对象(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)
于 2011-01-25T01:24:26.880 回答
5

从 开始Scala 2.13,大多数集合现在都提供了一种partitionMap方法,该方法基于返回Right或的函数来划分元素Left

这允许我们基于类型(这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)
于 2018-10-07T09:37:42.043 回答
1

我在这里找不到这个基本问题的令人满意的解决方案。我不需要讲课,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])
于 2016-05-26T15:31:51.863 回答
0

像这样的东西可以帮助

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

于 2019-08-01T10:01:08.003 回答
0

我个人会为此使用 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
      }
    }
  }
于 2020-07-07T00:14:09.343 回答