【问题标题】:Do functional language compilers optimize a "filter, then map" operation on lists into a single pass?函数式语言编译器是否将列表上的“过滤,然后映射”操作优化为单遍?
【发布时间】:2021-08-17 11:40:05
【问题描述】:

我在工作日主要使用 TypeScript,在应用函数式模式时,我经常看到如下模式:

const someArray = anotherArray.filter(filterFn).map(transformFn)

此代码将过滤所有anotherArray 的项目,然后再次遍历过滤后的列表(如果没有过滤任何项目,则可能相同)并映射事物。换句话说,我们对数组进行了两次迭代。

这种行为可以通过使用reduce 对数组进行“单次传递”来实现:

const someArray = anotherArray.reduce((acc, item) => {
  if (filterFn(item) === false) {
    return acc;
  }
  acc.push(item);
  return acc;
}, [])

我想知道这种优化是否是编译器(在 TS 世界中)知道自动执行的操作,以及此类优化是否在更“功能优先”的语言(例如 Clojure 或 Haskell)中自动完成。例如,我知道函数式语言通常使用尾递归进行优化,所以我也想知道“过滤然后映射”的情况。这是编译器实际做的事情吗?

【问题讨论】:

  • 在存在副作用的情况下,转换不一定等效。
  • 这取决于纯洁和懒惰。 Layzness 在map/reduce 方面免费为您提供了更高的效率,但要懒惰并额外执行基于编译器的循环融合纯度是一项要求。只提供惰性数据结构但不像 Clojure 那样纯粹的语言需要转换器或其他方式来允许手动循环融合。
  • +1 我不时想知道同样的问题。此外,是否有任何编译器在这种纯流中利用潜在的并行性? :)

标签: functional-programming compiler-optimization


【解决方案1】:

首先,您通常不应该执着于一次性完成所有操作。在小型容器上,两次运行单操作循环和一次运行双操作循环之间没有太大区别。旨在编写易于理解的代码。一个 for 循环可能比两个更易读,但 reduce 并不比 filter 和 map 更易读。

编译器的作用取决于您的“容器”。当你的循环足够大以关心执行时间时,它通常也足够大以关心内存消耗。因此,在处理下一个元素之前,过滤然后映射到类似observable 的东西一次只作用于一个元素,一直通过管道。这意味着您只需要一个元素的内存,即使您的 observable 可能是无限的。

【讨论】:

  • +1 表示不关注开销效率而不是增加简单性。话虽如此,库,如果不是现在的编译器,确实会满足这些细节和边缘情况,所以如果真的需要避免开销,它是在库中开箱即用的。 :) 在大多数情况下,流操作可能不会效率低下,但有些应用程序有很多 100 条业务规则,并且它们的处理会累积起来。在这种情况下,最好只关注简单的流操作,让库/编译器担心这些低效率。
猜你喜欢
  • 1970-01-01
  • 2018-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-12
  • 2011-10-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多