将if-else之类嵌套循环重构为函数式管道 - XP123


嵌套结构难以阅读;管道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 等:)。

  • 更容易理解:您需要维护的上下文更少。
  • 潜在的内存效率更高:管道在处理流时与在集合上工作一样愉快。
  • 更容易并行化:您可以将每个阶段想象成自己的计算机。(不幸的是,现实生活中的并行化比这更难。)

具有这些管道的语言之间有很多重叠:它们通常提供相似的功能,即使它们稍微更改了名称。