<td>
<input...>
<a href="#">Something</a>
</td>
(FP中的reduce
)是Javascript中最常用的迭代高阶函数。例如,您可以foldL
来实现map
或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;
map(inc)(xs); // [2, 3, 4, 5]
派生some
或every
,因为两者都可以提前返回。
那么更广义的局部缩减函数怎么样呢?到目前为止,我已经提出了以下天真实施:
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函数 - 或者至少是一个生成器。
答案 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