【问题标题】:How to implement a more general reduce function to allow early exit?如何实现更通用的reduce函数以允许提前退出?
【发布时间】:2016-04-30 20:06:21
【问题描述】:

reduce(在 FP 中也称为 foldL)是 Javascript 中最通用的迭代高阶函数。例如,您可以根据reduce 实现mapfilter。我使用命令式循环来更好地说明算法:

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]

但是您不能从reduce 派生出someevery,因为两者都可以提前返回。

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

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]

我也可以用foldLP来实现map

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

如果您查看调用堆栈,则归约函数acc =&gt; x =&gt; acc.concat([f(x)]) 的返回语句必须跳过几个堆栈帧。这种堆栈操作让我想到了延续。也许有一种有效的方法可以在继续传递样式中解决这个问题以及一个经过调整的 call/cc 函数 - 或者至少使用一个生成器。

【问题讨论】:

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


    【解决方案1】:

    事实证明,一旦你习惯了 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]) 之前,需要一个中间数组 ([1, 3, 5])。但这就是换能器的问题...

    【讨论】:

      【解决方案2】:

      惰性求值可以轻松解决这个问题。虽然我们在 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
      

      【讨论】:

      • foldR 首先从左到右构建递归定义的环境。这很令人困惑,因为它似乎是foldL。然后闭包的实际调用从右到左进行,如foldR 所示。 foldR的评估示意图如下:f(1)(f(2)(f(3)(f(4)(acc))))。如果闭包从右到左嵌套,就会得到一个反向数组。
      • 是的,在函数式编程中 foldrfoldl 的区别在于关联性,而不是交换性。 JavaScript 的 reduceRight 搞错了 :-)
      • 也许我错了,但foldR 以及早期退出和减少的函数不是[a] -&gt; Bool 类型,而是例如[a] -&gt; [b] 会导致问题,因为提前退出总是返回 false,然后这个值也会减小。这在减少[a] -&gt; [Bool] 类型的函数时尤其会出现问题,因为无法将常规布尔值与可能提前退出的布尔值区分开来。
      • 我不明白你所说的“提前退出总是返回false,然后这个值也会减小”是什么意思?此外,我不确定您的示例类型 [a] -&gt; [Bool] 的函数会是什么样子。
      • “短路”专门指用于布尔值的 ||/&amp;&amp; 运算符。当然,您不能将它们用于数组。 takeN 的减速器应该是 x =&gt; acc =&gt; n-- ? [x].concat(acc()) : [] (除了 takeN can't easily implemented with foldR 纯粹的事实)
      猜你喜欢
      • 2012-12-19
      • 1970-01-01
      • 1970-01-01
      • 2016-10-13
      • 2019-02-21
      • 1970-01-01
      • 1970-01-01
      • 2021-01-26
      • 2017-08-28
      相关资源
      最近更新 更多