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

时间:2016-01-24 14:27:06

标签: javascript arrays functional-programming reduce control-flow

<td> <input...> <a href="#">Something</a> </td> (FP中的reduce)是Javascript中最常用的迭代高阶函数。例如,您可以foldL来实现mapfilter。我使用命令式循环来更好地说明算法:

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; map(inc)(xs); // [2, 3, 4, 5] 派生someevery,因为两者都可以提前返回。

那么更广义的局部缩减函数怎么样呢?到目前为止,我已经提出了以下天真实施:

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]; foldLP(append)(takeN(3))([])(xs); // [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的return语句必须跳过几个堆栈帧。这种堆栈操作让我想到了延续。也许有一种有效的方法可以在Continuation Passing Style中解决这个问题,同时还有一个改编的call / cc函数 - 或者至少是一个生成器。

2 个答案:

答案 0 :(得分:4)

事实证明,一旦你习惯了CPS,就可以很容易地实现减少的概括:

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])。但那是换能器的问题......

答案 1 :(得分:1)

懒惰的评估解决了这个问题。虽然我们在JavaScript中没有这个功能,但我们可以通过传递函数而不是值来模拟它:

const foldR = f => acc => xs => xs.length
  ? f(xs[0])(() => foldR(f)(acc)(xs.slice(1)))
  : acc //   ^^^^^ "lazy"

const map = f => foldR(x => acc => [f(x)].concat(acc()))([])
const every = f => foldR(x => acc => f(x) && acc())(true)
//                                        ^^^^^^^^ short-circuited - f(x) ? acc() : false

let xs = [1, 2, 3, 4];
console.log(map(x => x+1)(xs)); // [2, 3, 4, 5]
console.log(every(x => x%2==0)(xs)); // false

另一种方法是使用CPS,您可以轻松跳转到函数的末尾:

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

const map = f => foldL(acc => x => cont => f(x)(res => cont(acc.concat([res]))))([]);
//const every = f => // xs => cont =>
//            foldL(acc => x => c => f(x)(res => c(acc && res)))(true) // (xs)(cont)
//                                   ^^^^ eager call
const every = f => xs => cont =>
              foldL(acc => x => c => acc ? f(x)(c) : cont(false))(true)(xs)(cont)
//                 call only when acc=true ^^^^      ^^^^^^^^^^^ jump out early otherwise
let xs = [1, 2, 3, 4];
let inc = x => cont => cont(x+1);
map(inc)(xs)(console.log.bind(console)); // [2, 3, 4, 5]

let even = x => cont => cont(x%2==0)
every(even)(xs)(console.log.bind(console)); // false