Scala循环选择:功能循环与传统for循环

时间:2012-08-23 03:55:29

标签: scala

使用功能构造(map,foreach,flatMap等)更好地循环集合吗? 作为一个虚拟问题,考虑我有一个字符串列表,我想按不同的标准过滤字符串,然后映射它们以获得一些价值。 请考虑以下代码:

val x1 = list.filter(criteria1).map(do_something)
val x2 = list.filter(criteria2).map(do_something)

假设我有5个不同的过滤条件,那么我就会循环遍历列表(可能很大)10次(一次使用过滤器,一次使用地图)。

但是,我可以将这些全部组合成一个for循环并在一次迭代中返回/填充5个新列表,然后映射每个列表总共6个循环而不是10个。

for(i<- 0 to list.length-1){
  if(criteria1) //filter
  if(criteria2) //filter
}

这段代码可能会迫使我使用可变列表,但从性能的角度来看,在这种情况下使用函数结构是否有意义。 哪种方法更好?

注意:上面的代码/问题只是作为一个例子,我希望它能解释我所指的那种情况

6 个答案:

答案 0 :(得分:6)

如果您要过滤和映射,可以使用withFilter代替filter,这会使过滤器变得懒惰,这样您就不会多次遍历列表。 for - 表达式使用withFilter来提高效率。您还可以查看view s,它为其他操作提供类似的懒惰。

从问题中你想要做什么并不完全清楚,但是我想你希望根据不同的过滤器和地图操作输出5个新列表。如果性能至关重要,那么使用像你建议的循环和可变构建器是一种合理的方法,这就是编程了多少个集合方法(检查源代码)。不确定为什么你认为你需要过滤到5个列表然后遍历每个列表来进行映射 - 为什么不在构建新列表的同时做地图,方法是将函数应用于每个元素?例如

  def split[T](xs: Seq[T])(ops: (T => Boolean, T => T)*): Seq[Seq[T]] = {
    val (filters, maps) = ops.unzip
    val buffers = IndexedSeq.fill(ops.size)(ListBuffer.empty[T])
    for {
      x <- xs
      i <- buffers.indices
      if filters(i)(x)
    } buffers(i) += maps(i)(x)  
    buffers.map(_.toSeq)  // return to immutable-land
  }

  // demo: 
  val res = split(1 to 10)(
    (_ < 5, _ * 100),     // multiply everything under 5 by 100
    (_ % 2 == 1, 0 - _),  // negate all odd numbers
    (_ % 3 == 0, _ + 5)   // add 5 to numbers divisible by 3
  )

  println(res) 
  //Vector(List(100, 200, 300, 400), List(-1, -3, -5, -7, -9), List(8, 11, 14))

我认为没有一种内置方法可以做你想做的事情(我想)。请注意,如果使用递归,则可以定义不具有可变状态的构建器方法,但这是本地可变状态更简洁/可读的地方。

您的问题实际上取决于性能,并且过早优化很容易。如果您确实遇到真正的性能问题,我建议您只执行上述操作。如果惯用/简单不够好,那么你可以调整一些东西来优化你的特定用例。它可以归结为这样一个事实:对于您可能想要做的所有事情,不能有内置的优化方法。

答案 1 :(得分:5)

你也可以这样做:

val x1 = for(x <- list if criteria1) yield do_something(x)

编译器实际上将此转换为val x1 = list.filter(criteria1).map(do_something),就像上面一样。 for理解只是一些很好的语法糖,它允许您将某些序列上的复杂操作聚合转换为更具可读性的内容。您可以阅读the relevant chapter in Odersky's book了解更多详情。

回到你的问题。如果您尝试根据不同的过滤器和地图生成5个不同的列表,则可能应该列出列表。您可以使用for推导来遍历每对转换函数的输入列表。

这将有助于您使代码更简单,但它实际上不会降低问题的算法复杂性(即您仍然会在列表上迭代5次)。

在这种情况下,我认为你是正确的,使用命令式循环会更有效率。用于构建列表的推荐数据结构是ListBuffer,因为您可以在常量时间内将元素添加到任一端 - 然后当您构建完列表时,可以将其转换为不可变列表(也在常量中)时间)。奥德斯基的书中还有一个小section on using ListBuffer。我就是这样做的:

import scala.collection.mutable.ListBuffer

val b1 = new ListBuffer[Int]
val b2 = new ListBuffer[Int]
// ... b3, b4, b5

for (x <- list) {
  val y = do_something(x)
  if (criteria1(x)) b1 += y
  if (criteria2(x)) b2 += y
  // ... criteria3, criteria4, criteria5
}

val x1 = b1.toList
val x2 = b2.toList
// ... x3, x4, x5

因为它使用了一个可变的ListBuffer,所以这段代码不再是“纯粹的” - 但是对于长列表来说可能值得加速,因为你不再需要遍历整个列表5次。

在这种情况下,我不会说一种方法比另一种方法好得多。 ListBuffer方式使用突变,这种方法更快但可能会使代码难以维护。相比之下,功能越多的版本只是在原始列表中使用对filtermap的重复调用,这可能更容易阅读(假设读者当然熟悉惯用的Scala)并且更易于维护,但可能运行得慢一点。这个选择实际上取决于你的目标。

答案 2 :(得分:1)

我不太确定多次查看列表会变慢。您必须从长度为m的列表中构建长度为k的{​​{1}}列表。因此,无论如何,您都必须对n中的每一个进行m*k次比较。如果它比较慢,那么它是由一些常数因素决定的。我不知道这个因素是小还是大。

如果你真的想一次性完成,那肯定是可能的。 列表中的任何操作都可以通过折叠一次完成。它可能有点复杂,并强调为什么它可能不会更快。这当然难以理解:

n

您可能需要比我在此处给出的更多类型注释。

你也可以在这里使用懒惰:

val cs = List((criteria1, f1), (criteria2, f2))
val xs = list.foldRight(cs.map(_ => Nil)) { (x, rs) =>
  (cs zip rs).map { case ((p, f), r) =>
    if (p(x)) f(x) :: r else r
  }
}

这遍历列表次。在您请求结果元素之前,元素实际上不会被过滤和映射。显然使用您的真实代码而不是list.toStream.filter(???).map(???)

答案 3 :(得分:1)

迭代部分与您的表现真的相关吗?在大多数情况下,我怀疑。只有在这种情况下,单个for循环才会更快。

但是如果你必须使用可变数据类型,那么现在很难在多个内核上运行,如果这确实处于性能危急情况,那么在8-800内核上运行它所获得的收益将是通过保存一个循环迭代获得的巨大收益。

请注意,for comprehension通常不是最佳性能,因为它可能需要创建大量的闭包实例。

答案 4 :(得分:1)

如果我理解正确,您希望根据不同的标准从单个列表中创建多个列表。我认为groupBy可以达到目的〜

val grouped = list.groupBy{ item => {
    val c1 = criteria1(item)
    val c2 = criteria2(item)
    if (c1 && c2) 12
    else if (c1) 1
    else if (c2) 2
    else 0
}}
val excluded0 = grouped - 0
val result = excluded0 mapValues do_something
val x1 = result(1) ++ result(12)
val x2 = result(2) ++ result(12)

正如Apocalisp所提到的,你也可以使用viewforce来利用懒惰:

val grouped = list.view.groupBy{ ...
...
val x1 = (result(1) ++ result(12)).force

答案 5 :(得分:0)

如前所述,您可能还需要考虑通过filter以较短的形式提供mapcollect的组合。所以你可以这样做:

list.collect {
  case x if criteria1(x) => ...
  case x if criteria2(x) => ....
  case _ => ...
}

但是,对于同时满足criteria1criteria2的列表元素,这是一个稍微改变的语义。与Chris提出的方法类似,您可以创建第一个case x if criteria1(x) && criteria2(x),但当然不会扩展到多个此类标准。

您不清楚的一点是,如果您想构建实际结果列表(如第一个示例中所示),或者只是执行一些副作用(如第二个示例中所示)。后者也可以通过稍微不同的方法来实现,如以下示例所示:

// A list of criteria and corresponding effects
val criteriaEffects = List( 
  ( (x : Int) => x == 0, (x : Int) => { println("Effect 1: " + x) } ),
  ( (x : Int) => x == 1, (x : Int) => { println("Effect 2: " + x) } ) )

// now run through your values list
List(0,1,2).map(x => criteriaEffects.map( p => if (p._1(x)) p._2(x) ) )