如何实现更通用的reduce函数以允许提前退出?

2024-01-10

reduce (aka foldL(FP) 是 Javascript 中最通用的迭代高阶函数。例如,您可以实施map or filter按照reduce。我使用了命令式循环来更好地说明该算法:

const foldL = f => acc => xs => {
  for (let i = 0; i < xs.length; i++) {
    acc = f(acc)(xs[i]);
  }

  return acc;
};

const map = f => xs => {
  return foldL(acc => x => acc.concat([f(x)]))([])(xs);
}

let xs = [1, 2, 3, 4];
const inc = x => ++x;

const result = map(inc)(xs);

console.log(result);  // [2, 3, 4, 5]

但你无法得出some or every from reduce,因为两人都能够提前返回。

那么更通用的部分归约函数是什么样子的呢?到目前为止,我已经提出了以下简单的实现:

const foldLP = f => pred => acc => xs => {
  for (let i = 0, r; i < xs.length; i++) {
    r = pred(i, acc, xs[i]);

    if (r === true) { // normal iteration
      acc = f(acc)(xs[i]);
    } else if (r === false) { // early exit
      break;
    } /* else { // skip iteration
      continue;
    } */
  }

  return acc;
};

const takeN = n => (idx, acc, x) => idx < n;
const append = xs => ys => xs.concat(ys);

let xs = [1,2,3,4,5];
const result = foldLP(append)(takeN(3))([])(xs);

console.log(result); // [1,2,3]

我也可以实现map按照foldLP:

const always = x => y => x;
const map = f => xs => {
  return foldLP(acc => x => acc.concat([f(x)]))(always(true))([])(xs);
}

map(inc)(xs); // [2,3,4,5,6]

缺点是显而易见的:每当不需要提前退出机制时,always被不必要地调用。转换和提前退出函数静态地由以下组成foldLP并且不能独立使用。是否有更有效的组合器,可以实现广义的Array.prototype.reduce?

如果你看一下调用堆栈,就会发现reduce函数的return语句acc => x => acc.concat([f(x)])必须跳过几个堆栈帧。这种堆栈操作让我想到了延续。也许有一种有效的方法可以在延续传递风格中解决这个问题,并结合改编的 call/cc 功能 - 或者至少使用生成器。


事实证明,一旦你习惯了 CPS,reduce 的泛化就可以很容易地实现:

const foldL = f => acc => xs => xs.length
 ? f(acc)(xs[0])(xss => foldL(f)(xss)(xs.slice(1)))
 : acc;

const map = f => foldL(acc => x => cont => cont(acc.concat([f(x)])))([]);
const filter = pred => foldL(acc => x => cont => cont(pred(x) ? acc.concat([x]) : acc))([]);
const every = pred => foldL(acc => x => cont => pred(x) ? cont(true) : false)(true);
const some = pred => foldL(acc => x => cont => pred(x) ? true : cont(false))(false);
const takeN = n => foldL(acc => x => cont => acc.length < n ? cont(acc.concat([x])) : acc)([]);

const comp = f => g => x => f(g(x));
const not = f => x => !f(x);
const inc = x => ++x;
const odd = x => x & 1;
const even = not(odd);
const lt3 = x => x < 3;
const eq3 = x => x === 3;
const sqr = x => x * x;
const xs = [1, 2, 3, 4, 5];

map(inc)(xs); // [2, 3, 4, 5, 6]
filter(even)(xs); // [2, 4]
every(lt3)(xs); // false
some(lt3)(xs); // true
takeN(3)(xs); // [1, 2, 3]

// we can compose transforming functions as usual
map(comp(inc)(sqr))(xs); // [2, 5, 10, 17, 26]

// and the reducing functions as well
comp(map(inc))(filter(even))(xs); // [3, 5]
comp(takeN(2))(filter(odd))(xs); // [1, 3]

正如您所看到的,这并不是真正纯粹的 CPS,而是与 Direct Style 的混合。这样做有一个很大的好处foldL通常的转换函数必须不携带任何额外的延续参数,但保持其正常的签名。

我仅在部分代码中使用 CPS 函数,它们在实现所需行为方面是不可替代的。 CPS 是一个极其强大的构造,您总是会尽可能使用最不表达的构造。

comp(takeN(2))(filter(odd))(xs)说明了实施的弱点之一(可能还有其他弱点)。归约函数的组合不会发生在数组元素的级别。因此需要一个中间数组([1, 3, 5])在计算最终结果之前([1, 3])。但这是传感器的问题......

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何实现更通用的reduce函数以允许提前退出? 的相关文章

随机推荐