对于这种情况,函数式编程效率较低吗?

时间:2016-02-11 19:31:26

标签: javascript functional-programming

我正在阅读“Javascript中的函数式编程”一书。

在第2章中,命令性/功能性代码之间有以下比较,用于查找仅包含字符串中字母的前四个单词:

势在必行

var words = [], count = 0;
text = myString.split(' ');
for (i=0; count<4, i<text.length; i++) {
  if (!text[i].match(/[0-9]/)) {
    words = words.concat(text[i]);
    count++;
  }
}

功能

var words = [];
var words = myString.split(' ').filter(function(x){
    return (! x.match(/[1-9]+/));
}).slice(0,4);

我推断,对于text的长度大于4的任何情况,命令式版本会更快,因为它只运行到找到匹配的前四个单词标准,而功能版本首先过滤整个数组,然后将前四个元素分开。

我的问题是,我是否正确地假设这个?

2 个答案:

答案 0 :(得分:6)

在某些情况下(和你一样)是的,但并非总是如此。很多像Haskell或Scala这样的函数式语言都存在懒惰。这意味着不会立即评估函数,而只在需要时评估函数。

如果你熟悉Java 8,他们的Streams API也是懒惰的,这意味着这样的话,不会遍历整个流3次。

stream.filter(n -> n < 200)
    .filter(n -> n % 2 == 0)
    .filter(n -> n > 15);

这是一个非常有趣的概念,你可以在这里查看Scala Stream课程的文档http://www.scala-lang.org/api/2.10.0/index.html#scala.collection.immutable.Stream

答案 1 :(得分:3)

这两个代码片段的比较非常有意义 - 作为教程的一部分。函数式编程要求很高,如果作者没有用最有效的函数实现来面对他的读者,那么为了简化示例。

为什么函数式编程要求很高?因为它遵循数学原则(而这些原则并不总是人类的逻辑),因为新手习惯于习惯性的风格。在FP中,数据流具有优先权,而实际算法保留在后台。习惯这种风格需要时间,但如果你曾经做过一次,你可能永远都不会回头!

如何以功能方式更有效地实现此示例?有几种可能性,我举两个例子。请注意,两种实现都避免使用中间数组:

  1. Lazy Evaluation
  2. 严格评估Javascript。但是,可以使用thunks(nullary函数)模拟延迟评估。此外,foldR(向右折叠)需要作为迭代函数,从中导出filterN

    const foldR = rf => acc => xs => xs.length
     ? rf(xs[0])(() => foldR(rf)(acc)(xs.slice(1)))
     : acc;
    
    const filterN = pred => n => foldR(
      x => acc => pred(x) && --n ? [x].concat(acc()) : n ? acc() : [x]
    )([]);
    
    const alpha = x => !x.match(/[0-9]/);
    let xs = ["1", "a", "b", "2", "c", "d", "3", "e"];
    
    filterN(alpha)(4)(xs); // ["a", "b", "c", "d"]
    

    此实现的缺点是filterN不纯,因为它是有状态的(n)。

    1. Continuation Passing Style
    2. CPS支持filterN的纯变体:

      const foldL = rf => acc => xs => xs.length
       ? rf(acc)(xs[0])(acc_ => foldL(rf)(acc_)(xs.slice(1)))
       : acc;
      
      const filterN = pred => n => foldL(
        acc => x => cont => pred(x)
         ? acc.length + 1 < n ? cont(acc.concat(x)) : acc.concat(x)
         : cont(acc)
      )([]);
      
      const alpha = x => !x.match(/[0-9]/);
      let xs = ["1", "a", "b", "2", "c", "d", "3", "e"];
      
      filterN(alpha)(4)(xs); // ["a", "b", "c", "d"]
      

      foldRfoldL的区别有点令人困惑。差异不在于交换性,而在于相关性。 CPS实施仍然存在缺陷。 filterN应分为filtertakeN,以提高代码的可重用性。

      1. Transducers
      2. 传感器允许编写(减少/转换)函数,而不必依赖中间数组。因此,我们可以将filterN分为两个不同的函数filtertakeN,从而提高其可重用性。遗憾的是,我还没有找到适合可理解和可执行示例的传感器的简洁实现。我将尝试开发自己的简化换能器解决方案,然后在此给出一个合适的例子。

        <强>结论

        如您所见,这些实现可能不如命令式解决方案有效。 Bergi已经指出,执行速度并不是函数式编程最关注的问题。如果微观优化对你很重要,你应该继续依赖命令式的风格。