【问题标题】:Real world examples of using reduceRight in JavaScript在 JavaScript 中使用 reduceRight 的真实示例
【发布时间】:2015-04-20 20:03:30
【问题描述】:

不久前,我在 StackOverflow 上发布了一个问题,显示 native implementation of reduceRight in JavaScript is annoying.因此,我创建了一个 Haskell 风格的 foldr 函数作为补救措施:

function foldr(array, callback, initial) {
    var length = array.length;

    if (arguments.length < 3) {
        if (length > 0) var result = array[--length];
        else throw new Error("Reduce of empty array with no initial value");
    } else var result = initial;

    while (length > 0) {
        var index = --length;
        result = callback(array[index], result, index, array);
    }

    return result;
}

但是,我从来没有使用过这个foldr 函数,因为我从来不需要从右到左遍历数组。这让我开始思考,为什么我不像在 Haskell 中那样在 JavaScript 中使用 foldr,在 JavaScript 中使用 foldr 的真实例子有哪些?

我可能是错的,但我相信 foldr 函数在 Haskell 中被广泛使用,因为:

  1. 懒惰评估 (foldl is tail recursive, so how come foldr runs faster than foldl?)
  2. 使用foldr/build (Correctness of short cut fusion: foldr/build) 进行短切融合

这可以解释为什么 foldrreduceRight 在 JavaScript 中没有被广泛使用。我还没有看到 foldr 在现实世界中仅用于从右到左的迭代顺序。

这让我想到了两个问题:

  1. 在 JavaScript 中使用 reduceRight 的实际示例有哪些?也许你在 npm 包中使用过它。如果您能将我链接到您的代码并解释为什么需要使用reduceRight 而不是reduce,那就太好了。
  2. 为什么reduceRight 没有像reduce 在JavaScript 中那样广泛使用?我已经在这件事上提供了我的两分钱。我相信foldr 主要只是因为它的惰性而使用,这就是为什么reduceRight 在JavaScript 中不是很有用的原因。但是,我可能是错的。

对于第一个问题,我试图找到一些在 JavaScript 中使用 reduceRight 的真实示例。但是,我没有找到任何令人满意的答案。我发现的唯一例子是琐碎的和理论上的:

when to use reduce and reduceRight?

我正在寻找的是一个实际的例子。什么时候在 JavaScript 中使用 reduceRight 而不是 reduce 可行?

对于第二个问题,我知道这主要是基于意见,这就是为什么你不回答也没关系。这篇文章的主要焦点是第一个问题,而不是第二个。

【问题讨论】:

  • 我不认为haskell中的foldr函数是从右到左遍历的?
  • @Sibi 你是对的。由于定义列表的方式(即data [a] = [] | a : [a]),在 Haskell 中无法从右到左遍历列表。 Haskell 中的foldr 函数只从右到左累积结果。但是,这个问题实际上与 Haskell 本身无关。
  • 我从来没有在代码中使用过reduceRight。我用过reduce 至少一千次。
  • 嗯,也许用它来实现某种reverse
  • 我最近对所有 cdn.js 脚本进行了代码调查。它发现 reduceRight 有 144 次使用,而 reduce 有 654 次使用。可悲的是,我扔掉了数据(花了 2 个小时来删除快速盒子上的所有脚本),否则我会找到你的细节,但如果 reduceRight 是使用的 1/4,它们应该在那里......

标签: javascript haskell iteration reduce fold


【解决方案1】:

要回答您的第一个问题,reduceRight 在您想以从左到右的方式指定项目但以从右到左的方式执行它们时非常方便。

考虑一下这种朴素的 compose 函数实现,它从左到右接受参数,但从右到左读取和执行:

var compose = function () {
    var args = [].slice.call(arguments);

    return function (initial) {
        return args.reduceRight(function (prev, next) {
            return next(prev);
        }, initial);
    }
}

与其在阵列上调用reverse 占用时间/空间,不如更简单、更轻松地理解reduceRight 调用。

使用此compose 函数的示例如下所示:

var square = function (input) {
    return input * input;
};

var add5 = function (input) {
    return input + 5;
};

var log = function (input) {
    console.log(input);
};

var result = compose(log, square, add5)(1); // -> 36

我敢肯定还有更多关于 reduceRight 有用的技术示例,这只是其中之一。

【讨论】:

  • 确实,函数组合是从右到左读取的,因此在这种情况下使用reduceRight 是有意义的。但是,您可以轻松地从左到右读取函数组合,并改用reduce。我想这只是一个偏好问题。就个人而言,如果我要实现compose 函数,那么出于性能原因,我既不会使用reduce,也不会使用reduceRight。我不希望 twice the number of function call overheads 用于复合函数。正如你所说,可能有更好的例子。因此,我会等待它。
  • @[Jamie Dixon] 你是对的,它是一个方便的实用程序,它充当 Array.reverse 的代理
【解决方案2】:

您是绝对正确的,以至于我不完全确定这甚至是一个真正的问题。懒惰和融合都是在 Haskell 中首选 foldr 的两个巨大原因。这两个东西在 JavaScript 的数组中都不存在,所以实际上没有理由在现实世界的 JavaScript 中使用 reduceRight。我的意思是,您可以设计一个案例,通过将事物推到末尾来构造一个数组,然后您想在累积结果的同时从最新到最旧迭代它们。但 imo 这太做作了。


只是为了说明事物的 Haskell 方面。请注意,在 Haskell 中,右折叠实际上执行从右到左的评估。您可以考虑从右到左分组评估,但由于懒惰,这不是计算的。考虑:

foldr (\a _ -> Just a) undefined [1..]

我为累加器提供了一个undefined 起始值,以及要折叠的无限自然数列表。哦亲爱的。然而这一点都不重要。这个表达式很高兴地计算为Just 1

从概念上讲,分组是这样工作的:

let step a _ = Just a
let foldTheRest = foldr step undefined [2..]
step 1 foldTheRest

考虑到分组,我们“折叠其余部分”,然后将step 函数应用于两个参数:“列表的第一个元素”和“折叠列表的其余部分得到的任何东西”。然而,由于步进函数甚至不需要它的累加器参数,所以这部分计算永远不会被评估。评估这个特定折叠所需的只是“列表的第一个元素”。


重申一下,JavaScript 数组没有保留 Haskell 所享有的 foldr 的好处,因此实际上没有理由使用 reduceRight。 (相比之下,Haskell 有时有充分的理由使用严格的左折叠。)

n.b.我不同意您的另一个问题,您得出的结论是“reduceRight 的本机实现是错误的”。我同意他们选择了他们所做的参数顺序是烦人,但这并不是天生的错误

【讨论】:

    【解决方案3】:

    当您尝试从给定数组创建新的尾项数组时,请使用 reduceRight

    var arr = [1,2,2,32,23,4,5,66,22,35,78,8,9,9,4,21,1,1,3,4,4,64,46,46,46,4,6,467,3,67];
    
    function tailItems(num) {
        var num = num + 1 || 1;
        return arr.reduceRight(function(accumulator, curr, idx, arr) {
            if (idx > arr.length - num) {
                accumulator.push(curr);
            }
            return accumulator;
        }, []);
    }
    
    console.log(tailItems(5));
    
    //=> [67, 3, 467, 6, 4]
    

    reduceRight 另一个有用的有趣之处在于,由于它反转了它正在执行的数组,投影函数的 index 参数提供了从 arr.length 开始的数组索引,例如:

    var arr = [1,2,2,32,23];
    
    arr.reduceRight(function(accumulator, curr, idx, arr) {
        console.log(idx);
    }, '');
    
    // => 4,3,2,1,0
    

    如果您尝试通过索引查找特定数组项,即arr[idx],它位于数组的尾部,这可能很有用,显然数组越大越实用 - 想想数千个项目。

    也许一个很好的例子是社交媒体提要,它首先显示最新的 10 项,并且可能会按需加载更多。考虑一下reduceRight 在这种情况下如何以相反的顺序组织数组集合以适应这个例子。

    【讨论】:

    • 很好的例子,Elise。
    • 这些都是不好的例子,因为它们都在滥用折叠操作:首先,您要更改单个 accumulator 值,然后您要打印内容。这意味着这两种情况实际上都没有折叠,它们只是迭代。
    【解决方案4】:

    此外,如果您需要从内到外构建嵌套计算/数据结构,您可以使用reduceRight。而不是手动编写

    const compk = f => g => x => k => f(x) (x => g(x) (k));
    
    const compk3 = (f, g, h) => x => k => f(x) (x => g(x) (y => h(y) (k)));
    
    const inck = x => k => setTimeout(k, 0, x + 1);
    
    const log = prefix => x => console.log(prefix, x);
    
    compk3(inck, inck, inck) (0) (log("manually")); // 3

    我想应用一个构建递归结构的编程解决方案:

    const compkn = (...fs) => k => fs.reduceRight((chain, f) => x => f(x) (chain), k);
    
    const inc = x => x + 1;
    
    const lift = f => x => k => k(f(x));
    
    const inck = x => k => setTimeout(k, 0, x + 1);
    
    const log = prefix => x => console.log(prefix, x);
    
    compkn(inck, lift(inc), inck) (log("programmatically")) (0); // 0

    【讨论】:

      【解决方案5】:

      Array.reduceRight() 在以下情况下很棒:

      • 您需要遍历一个项目数组来创建 HTML
      • 并且需要 HTML 中的计数器之前项目

      .

      var bands = {
          Beatles: [
              {name: "John", instruments: "Guitar"},
              {name: "Paul", instruments: "Guitar"},
              {name: "George", instruments: "Guitar"},
              {name: "Ringo", instruments: "Drums"}]
      };
      function listBandplayers(bandname, instrument) {
          var bandmembers = bands[bandname];
          var arr = [  "<B>" , 0 , ` of ${bandmembers.length} ${bandname} play ` , instrument , "</B>",
                      "\n<UL>" , ...bandmembers , "\n</UL>" ];
          var countidx = 1;
          return arr.reduceRight((html, item, idx, _array) => {
                  if (typeof item === 'object') {
                      if (item.instruments.contains(instrument)) _array[countidx]++;
                      item = `\n\t<LI data-instruments="${item.instruments}">` + item.name + "</LI>";
                  }
                  return item + html;
          });
      }
      console.log( listBandplayers('Beatles', 'Drums') );
      /*
      <B>1 of 4 Beatles play Drums</B>
      <UL>
          <LI data-instruments="Guitar">John</LI>
          <LI data-instruments="Guitar">Paul</LI>
          <LI data-instruments="Guitar">George</LI>
          <LI data-instruments="Drums">Ringo</LI>
      </UL>
      */
      console.log( listBandplayers('Beatles', 'Guitar') );
      /*
      <B>3 of 4 Beatles play Guitar</B>
      <UL>
          <LI data-instruments="Guitar">John</LI>
          <LI data-instruments="Guitar">Paul</LI>
          <LI data-instruments="Guitar">George</LI>
          <LI data-instruments="Drums">Ringo</LI>
      </UL>
      */
      

      【讨论】:

        【解决方案6】:

        Array.prototype.reduceRight 在 Javascript 中实际上是有用的,即使有严格的评估。为了了解为什么让我们看一下 Javascript 中的地图转换器和一个简单的示例。由于我是一名函数式程序员,我的版本主要依赖于柯里化函数:

        const mapper = f => g => x => y => g(x) (f(y));
        
        const foldl = f => acc => xs => xs.reduce((acc, x) => f(acc) (x), acc);
        
        const add = x => y => x + y;
        
        const get = k => o => o[k];
        
        const len = get("length");
        
        const concatLen = foldl(mapper(len) (add)) (0);
        
        const xss = [[1], [1,2], [1,2,3]];
        
        console.log(concatLen(xss)); // 6

        mapper (f =&gt; g =&gt; x =&gt; y =&gt; g(x) (f(y))) 本质上是第二个参数中的函数组合。当我们回忆起右折叠 ((a -&gt; b -&gt; b) -&gt; b -&gt; [a] -&gt; b) 在 reducer 中翻转了参数时,我们可以用正常的函数组合替换 mapper。由于对 arity 的抽象,我们也可以很容易地用一元函数组成一个二元函数。

        不幸的是,Array.prototype.reducdeRight 的参数顺序被破坏了,因为它期望累加器作为 reducer 的第一个参数。所以我们需要用一个看起来很奇怪的 lambda 来解决这个问题:

        const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
        
        const comp = (f, g) => x => f(g(x));
        
        const add = x => y => x + y;
        
        const len = xs => xs.length;
        
        const concatLen = foldr(comp(add, len)) (0);
        
        const xss = [[1], [1,2], [1,2,3]];
        
        console.log(concatLen(xss));

        我认为这是一项重大改进,因为我们降低了实际理解代码的精神负担(compmapper 更常见)并且更加干燥。

        【讨论】:

          猜你喜欢
          • 2012-05-23
          • 1970-01-01
          • 2019-11-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-10-26
          • 2021-09-20
          • 2013-10-19
          相关资源
          最近更新 更多