【问题标题】:Function Composition - Isn't looping over an array multiple times for multiple operations inefficient?函数组合 - 多次遍历数组以进行多次操作不是效率低下吗?
【发布时间】:2020-05-13 16:39:33
【问题描述】:

我正在尝试了解函数式编程的概念和基础知识。我不会硬核 Haskell、Clojure 或 Scala。相反,我使用的是 JavaScript。

所以,如果我理解正确的话,函数式编程背后的想法是使用纯函数编写软件应用程序——它负责处理应用程序中的单一职责/功能,而不会产生任何副作用。

组合以这样一种方式进行,即一个函数的输出作为输入传递给另一个函数(根据逻辑)。

我分别编写了 2 个函数用于加倍和递增,它们以整数作为参数。后跟一个实用函数,该函数组成作为参数传入的函数。

{
    // doubles the input
    const double = x => x * 2

    // increments the input
    const increment = x => x + 1

    // composes the functions
    const compose = (...fns) => x => fns.reduceRight((x, f) => f(x), x)

    // input of interest
    const arr = [2,3,4,5,6]

    // composed function
    const doubleAndIncrement = compose(increment, double)

    // only doubled
    console.log(arr.map(double))

    // only incremented
    console.log(arr.map(increment))

    // double and increment
    console.log(arr.map(doubleAndIncrement))
}

输出如下:

[4, 6, 8, 10, 12]  // double
[3, 4, 5, 6, 7]    // increment
[5, 7, 9, 11, 13]  // double and increment

所以,我的问题是,在这种情况下,reduceRight 函数将遍历数组两次以应用这 2 个函数。

如果数组变大了,会不会效率低下?

使用循环,它可以在一次遍历中完成,两个操作在同一个循环中。

如何防止这种情况发生,或者我的理解是否有任何错误?

【问题讨论】:

  • 请注意,您可以通过在 doubleincrement 函数中添加日志来测试情况并非如此
  • 天啊!是的,你是对的......就像我完全沉浸在 FP 思维和单一责任的事情中,完全错过/忽略了这些基本的东西
  • 你也缺少操作融合的概念。许多功能平台将通过仅访问每个元素一次并应用所有mapped 操作来融合操作。这需要一些积极的优化技术,但在 GHC Haskell 编译器中实现。

标签: javascript node.js function ecmascript-6 functional-programming


【解决方案1】:

遍历数组的是map,并且只发生一次。 reduceRight 正在遍历组合函数列表(在您的示例中为 2),并通过该函数链将数组的当前值线程化。您描述的等效低效版本是:

const map = f => x => x.map(f)
const doubleAndIncrement = compose(map(increment), map(double))

// double and increment inefficient
console.log(doubleAndIncrement(arr))

这揭示了map 必须满足的laws 之一,即:

map(compose(g, f)) 等价于(同构)compose(map(g), map(f))

但正如我们现在所知,后者可以通过将其简化为前者来提高效率,并且它只会遍历输入数组一次。

【讨论】:

  • 这只有在您坚持使用map 时才有效,即保留结构。如果您还想过滤或只取第一个/最后一个元素怎么办?那么你需要换能器。
【解决方案2】:

优化时间复杂度同时仍然拥有小型专用函数是 fp 中广泛讨论的话题。

让我们来看这个简单的案例:

const double = n => 2 * n;
const square = n => n * n;
const increment = n => 1 + n;

const isOdd = n => n % 2;


const result = [1, 2, 3, 4, 5, 6, 7, 8]
  .map(double)
  .map(square)
  .map(increment)
  .filter(isOdd);
  
console.log('result', result);

在线性组合和链接中,这个操作可以读作O(4n)时间复杂度...这意味着对于每个输入,我们大约执行 4 个操作(例如,在 40 亿个项目的列表中,我们将执行 160 亿个操作)。

我们可以通过将所有操作(double、square、increment、isOdd)嵌入到单个 reduce 函数中来解决这个问题(中间值 + 不必要的操作数)……但是,这会导致可读性下降。

在 FP 中,您拥有 Transducer 的概念(请阅读 here),这样您仍然可以保持单一用途函数所赋予的可读性,并具有执行尽可能少的操作的效率。

const double = n => 2 * n;
const square = n => n * n;
const increment = n => 1 + n;

const isOdd = n => n % 2;

const transducer = R.into(
  [], 
  R.compose(R.map(double), R.map(square), R.map(increment), R.filter(isOdd)),
);

const result = transducer([1, 2, 3, 4, 5, 6, 7, 8]);

console.log('result', result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>

【讨论】:

    猜你喜欢
    • 2015-09-22
    • 2013-11-15
    • 1970-01-01
    • 2012-11-06
    • 1970-01-01
    • 2021-12-06
    • 2021-04-09
    • 1970-01-01
    • 2023-01-20
    相关资源
    最近更新 更多