簡體   English   中英

Scala循環選擇:功能循環與傳統for循環

[英]Scala looping choice : functional looping vs traditional for loop

是否使用功能構造(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
}

這段代碼可能會迫使我使用可變列表,但從性能的角度來看,在這種情況下使用函數結構是否有意義。 哪種方法更好?

注意:上面的代碼/問題只是作為一個例子,我希望它能解釋我所指的那種情況

如果您要過濾和映射,可以使用withFilter而不是filter ,這會使過濾器變得懶惰,這樣您就不會多次遍歷列表。 for withFilter使用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))

我不認為有一種內置方法可以做你想做的事情(我想)。 請注意,如果使用遞歸,則可以定義不具有可變狀態的構建器方法,但這是本地可變狀態更簡潔/可讀的地方。

您的問題實際上取決於性能,並且很容易過早地進行優化。 如果您確實遇到真正的性能問題,我建議您只執行上述操作。 如果慣用/簡單不夠好,那么你可以調整一些東西來優化你的特定用例。 它歸結為這樣一個事實:對於您可能想要做的所有事情,不能有內置的優化方法。

你也可以這樣做:

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

編譯器實際上將它轉換為val x1 = list.filter(criteria1).map(do_something) ,就像你上面一樣。 for comprehension只是一些很好的語法糖,它允許你將一些序列上的復雜操作聚合轉換成更具可讀性的東西。 您可以閱讀Odersky的書中的相關章節以獲取更多詳細信息。

回到你的問題。 如果您嘗試根據不同的過濾器和地圖生成5個不同的列表,則可能應該列出列表。 您可以使用for compreheres循環每對轉換函數的輸入列表。

這將有助於您使代碼更簡單,但它實際上不會降低問題的算法復雜性(即您仍然在列表上迭代5次)。

在這種情況下,我認為你是正確的,使用命令式循環會更有效率。 用於構建列表的推薦數據結構是ListBuffer因為您可以在常量時間內將元素添加到任一端 - 然后當您構建完列表時,可以將其轉換為不可變列表(也是在常量時間內)。 在Odersky的書中還有一小段關於使用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)並且更容易維護,但可能運行速度稍慢。 選擇實際上取決於您的目標。

我不太確定多次查看列表會變慢。 您必須從長度為n的列表中構建長度為k m列表。 所以你必須對n中的每一個進行m*k比較。 如果它比較慢,那么它是由一些常數因素決定的。 我不知道這個因素是小還是大。

如果你真的想一次性完成,那絕對是可能的。 列表上的任何操作都可以通過折疊一次完成。 它可能有點復雜,並強調為什么它可能不會更快。 這當然更難閱讀:

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(???)

這會遍歷列表次。 在您請求結果元素之前,元素實際上不會被過濾和映射。 顯然使用你的真實代碼而不是???

迭代部分與您的表現真的相關嗎? 在大多數情況下,我懷疑。 只有在這種情況下,單個for循環才會更快。

但是,如果你必須使用可變數據類型,那么現在很難在多個內核上運行,如果這確實處於性能危急情況,那么在8-800內核上運行它所獲得的收益將是巨大的從保存一個循環迭代中獲得的收益很少。

請注意,for comprehension通常不是最佳性能,因為它可能需要創建大量的閉包實例。

如果我理解正確,您希望根據不同的標准從單個列表中創建多個列表。 我認為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

如前所述,您可能還需要考慮通過collect以較短的形式提供filtermap的組合。 所以你可以這樣做:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM