將if-else之類巢狀迴圈重構為函式式管道 - XP123

banq發表於2021-08-21

巢狀結構難以閱讀;管道stream通常更容易閱讀和思考。
巢狀結構具有“厄運之箭”的感覺,您需要同時管理所有父結構的上下文;而管道stream通常是線性的。 
許多語言都新增了“函式式管道”風格,建立在首先在 Lisp 中探索的 map-filter-reduce 的基礎上,哦,大約 50 年前,大約 40 年前在 Unix 和 Smalltalk 中:)
在下面描述的內容在概念上適用於 Java、C#、Kotlin、Python 等。我將使用 Swift 中的示例,並解釋任何特定於 Swift 的構造。
我使用所有這三種方法:
  1. 工具:如果您的工具可以勝任,請使用該工具!例如,當 IntelliJ IDEA 看到一個知道如何轉換的迴圈時,它會彈出一個黃色的燈泡,提供執行此操作。 
  2. 提取新集合:當迴圈遍歷集合時,將集合提取到變數中作為新管道的種子,並逐漸將迴圈的部分移入其中,直到原始迴圈消失。Martin Fowler 在他的優秀書籍和文章(請參閱參考資料)中探討了這種方法,因此我不會進一步探討。
  3. 就地轉換:將迴圈轉換為就地管道,一次一個巢狀級別。我們將在下面使用這種方法。 

讓我們來看一個例子。我們將一步一步地將下面的迴圈變成一個函式性管道。該集合是一個陣列,包含字典(Map)。  

  var points = [ ["x":"17", "y":"23"], ["x": "x12", "y": "y100"], ["x": "3", " y": "2", "z": "11"], ["w":"21"]]


目標是計算任何具有 y 座標的條目的平均值。

  var sum = 0 
    var count = 0 

    for i in 0..<points.count { 
      let item = points[i] 
      if let y = item["y"] { 
        if let theInt = Int(y) { 
          sum += theInt
          計數 += 1 
        } 
      } 
    }


print(sum / count)
    


使用 forEach() 替代for迴圈:

 var sum = 0
    var count = 0
    
    points.forEach { item in
      if let y = item["y"] {
        if let theInt = Int(y) {
          sum += theInt
          count += 1
        }
      }
    }
    
    print(sum / count)


使用compactMap() 處理條件不滿足的情況:
  var sum = 0
    var count = 0
    
    points
      .compactMap { $0["y"] }
      .forEach { y in
        if let theInt = Int(y) {
          sum += theInt
          count += 1
        }
      }
    
    print(sum / count)

再次使用 compactMap()忽略格式錯誤的整數

    var sum = 0 
    var count = 0 

    points 
      .compactMap { $0["y"] } 
      .compactMap { Int($0) } 
      .forEach { theInt in 
        sum += theInt 
        count += 1 
      } 

    print(sum / count)


 

路徑 1:拆分迴圈
我們可能會看到並意識到我們的程式碼正在計算兩件事,並拆分迴圈。要做到這一點,我們將儲存常用的計算成一個新的集合,獨立重複迭代處理sum和count。

  let values = points
      .compactMap { $0["y"] }
      .compactMap { Int($0) }

    var sum = 0
    values
      .forEach { theInt in
        sum += theInt
      }

    var count = 0
    values
      .forEach { theInt in
        count += 1
      }

    print(sum / count)


在兩個迴圈中使用reduce() 

    let values = points
      .compactMap { $0["y"] }
      .compactMap { Int($0) }

    let sum = values.reduce(0, +)

    let count = values
      .map { _ in 1}
      .reduce(0, +)

    print(sum / count)


我們可以更輕鬆地計算計數:
let count = values.count
 

路徑 2:單管道
我們可能會認識到 sum 和 count 可以儲存在一個元組中(一個主要是匿名的物件):

  var tuple = (sum: 0, count: 0)

    points
      .compactMap { $0["y"] }
      .compactMap { Int($0) }
      .forEach { theInt in
        tuple = (sum: tuple.sum + theInt,
                 count: tuple.count + 1)
      }

    print(tuple.sum / tuple.count)





如果你像我一樣,這個元組看起來有點難看,而且可能令人困惑。我會省去你使用它的 reduce() 呼叫。相反,我們看到 sum 和 count 必須一起協調。聽起來像是放置實際物件的好地方:

 class Average {
    var sum = 0
    var count = 0

    var value : Int? {
      if count == 0 { return nil }
      return sum / count
    }

    func add(_ item: Int) -> Average {
      sum += item
      count += 1
      return self
    }
  }


現在可以使用reduce :

   let average = points
      .compactMap { $0["y"] }
      .compactMap { Int($0) }
      .reduce(Average(), { $0.add($1) })

    print(average.value!)


 

兩條路徑的比較
當它澄清程式碼時,拆分迴圈可能是一個很好的舉措(並且可能讓您在多個物件之間重新分配行為)但是它有一個缺點——如果您將部分工作儲存在一個集合中,您可能會強制一個真正的集合存在,使用需要的所有記憶體管理。
相比之下,將其保留為管道意味著可能永遠不會有集合。當然,我們的示例有一個陣列常量,但相同的管道適用於物件流,從不需要同時使用它們。
在這種情況下,新物件對我來說是勝利。我沒想到,但是這個小物件確實改進了程式碼。
 

結論
函式式管道通常勝過巢狀結構(while - if - while -if -if 等:)。

  • 更容易理解:您需要維護的上下文更少。
  • 潛在的記憶體效率更高:管道在處理流時與在集合上工作一樣愉快。
  • 更容易並行化:您可以將每個階段想象成自己的計算機。(不幸的是,現實生活中的並行化比這更難。)

具有這些管道的語言之間有很多重疊:它們通常提供相似的功能,即使它們稍微更改了名稱。

 

相關文章