在JavaScript中使用reduceRight的真实世界示例

时间:2015-02-19 12:56:56

标签: javascript haskell iteration reduce fold

前段时间,我在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 / buildCorrectness of short cut fusion: foldr/build
  3. 进行捷径融合

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

    这让我想到了两个问题:

    1. 在JavaScript中使用reduceRight的一些真实世界示例是什么?也许您已经在npm包中使用过它。如果您可以将我链接到您的代码并解释为什么需要使用reduce代替reduceRight,那就太棒了。
    2. 为什么reduce没有像foldr那样广泛使用?我已就此事提供了两分钱。我认为reduceRight主要仅用于其懒惰,这就是为什么reduceRight在JavaScript中不是很有用的原因。但是,我可能错了。
    3. 对于第一个问题,我试图找到一些在JavaScript中使用reduceRight的真实示例。但是,我没有找到任何令人满意的答案。我发现的唯一例子是微不足道的和理论上的:

      when to use reduce and reduceRight?

      我正在寻找的是一个实际的例子。何时在JavaScript中使用reduce代替{{1}}?

      是否切实可行

      对于第二个问题,我理解这主要是基于意见的,这就是为什么如果你不回答它就好了。这篇文章的主要焦点是第一个问题,而不是第二个问题。

6 个答案:

答案 0 :(得分:7)

要回答您的第一个问题,当您想要以从左到右的方式指定项目但以从右到左的方式执行时,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有用的技术示例,这只是一个。

答案 1 :(得分:4)

你是绝对正确的,我不完全确定这甚至是一个真正的问题。懒惰和融合都是巨大的原因,为什么在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数组保留 none Haskell所享有的foldr的好处,因此实际上没有理由使用reduceRight。 (相比之下,Haskell有时候有充分的理由使用严格的左侧折叠。)

n.b。我不同意你的另一个问题,你得出的结论是“reduceRight的本地实现是错误的”。我同意讨厌他们选择了他们所做的参数顺序,但它本身并不是错误的

答案 2 :(得分:2)

当您尝试从给定数组创建新的尾部项数组时使用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在这个实例中如何以相反的顺序组织数组的集合以适应这个例子。

答案 3 :(得分:1)

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

&#13;
&#13;
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
&#13;
&#13;
&#13;

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

&#13;
&#13;
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
&#13;
&#13;
&#13;

答案 4 :(得分:0)

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>
*/

答案 5 :(得分:0)

即使有严格的评估,

Array.prototype.reduceRight在Javascript中也很有用。为了了解为什么让我们看一下Javascript中的地图传感器和一个简单的例子。由于我是一名函数式程序员,我的版本主要依赖于curried函数:

&#13;
&#13;
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
&#13;
&#13;
&#13;

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

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

&#13;
&#13;
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));
&#13;
&#13;
&#13;

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