在 JavaScript 中使用 reduceRight 的真实示例

Real world examples of using reduceRight in JavaScript

不久前,我 post 在 Whosebug 上提出了一个问题,显示 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 以及在 [=] 中使用 foldr 的一些真实示例72=]?

我可能是错的,但我相信 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)
  3. 的捷径融合

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

这引出了我的两个问题:

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

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

when to use reduce and reduceRight?

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

关于第二个问题,我知道它主要是基于意见,这就是为什么你不回答也没关系。 post 主要关注第一个问题,而不是第二个问题。

您完全正确,以至于我什至不能完全确定这是一个真正的问题。懒惰和融合都是 巨大的 原因 foldr 在 Haskell 中更受欢迎。 JavaScript 的数组中没有这两个东西,因此实际上没有理由在现实世界中使用 reduceRight JavaScript。我的意思是,您可以想出这样一种情况,即通过将事物推到末尾来构造一个数组,然后您希望在累积结果的同时从最新到最旧迭代它们。但我觉得这很做作。


只是为了说明事情的 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

考虑到分组,我们 "fold the rest",然后我们将 step 函数应用于两个参数:"first element of the list" 和 "whatever we got from folding the rest of the list." 但是,由于步进函数甚至不需要它的累加器参数,那部分计算永远不会被评估。我们需要评估这个特定的折叠是 "first element of the list."


重申一下,JavaScript 数组保留了 none Haskell 享有的 foldr 的好处,所以有字面上没有理由使用 reduceRight。 (相比之下,Haskell 中有时有充分的理由使用严格的左折叠。)

n.b。我不同意你的另一个问题,你得出的结论是 "the native implementation of reduceRight is wrong." 我同意 烦人 他们选择了他们所做的论证顺序,但这并不是天生的 .

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

考虑这个 compose 函数的天真实现,它从左到右接受参数,但从右到左读取和执行:

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

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

与其调用数组上的 reverse 占用 time/space,更简单、更容易理解 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

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有用的有趣的事情是,由于它反转了它正在执行的数组,投影函数的索引参数提供了从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 在这种情况下如何以相反的顺序组织数组集合以适合此示例。

此外,如果您需要构建一个由内而外的嵌套 computation/data 结构,您还可以使用 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

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

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 => g => x => y => g(x) (f(y))) 本质上是第二个参数中的函数组合。当我们回忆起右折叠 ((a -> b -> b) -> b -> [a] -> b) 在 reducer 中翻转了参数时,我们可以用正常的函数组合替换 mapper。由于对元数的抽象,我们还可以轻松地将二元函数与一元函数组合起来。

不幸的是,Array.prototype.reducdeRight 的参数顺序被破坏了,因为它期望累加器作为减速器的第一个参数。所以我们需要用一个看起来很奇怪的 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 更常见)并且更干。