3

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

 2 years ago
source link: https://www.jdon.com/57131
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
将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 等:)。

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK