函数式编程 - then() 在链式 filter/map 调用之间

Functional Programming - then() between chained filter/map calls

我正在这样解析数据:

getData()
.filter(fn)
.filter(fn2)
.filter(fn3)
.map(fn4)

其中过滤器在概念上是分开的,并且执行不同的操作。

出于调试目的,是否有 JavaScript 库或包装 promise 的方法,以便我可以这样做:

getData()
.filter(fn)
.then((result) => { log(result.count); return result })
.filter(fn2)
.then(debugFn)    // extra chained debug step (not iterating through arr)
.filter(fn3)
.map(fn4)

或者这是一个反模式?

您可以使用 monkey-patch Array.prototype,但不推荐

只要你只用它来调试:

Array.prototype.debug = function (fn) {
    fn(this);
    return this;
};

// example usage
[1, 2, 3].map(n = > n * 2).debug(console.log).map(n => n * 3);

这不是一个承诺 - 你可能不需要异步 - 但会给你 .then 类似的行为。

向内置对象原型添加函数是有争议的,所以很多人可能会反对。但是,如果您真的希望能够按照您的要求进行操作,那可能是唯一的选择:

Object.defineProperty(Array.prototype, "examine", {
  value: function(callback) {
    callback.call(this, this);
    return this;
  }
});

然后您可以将 .examine(debugFn) 个调用放入 .filter() 个调用链中,如您所述。

我相信可以使用断点来调试这样的东西。

编辑

经过一番思考,我确信 这个问题已经由 V-for-Vaggelis 给出:只需使用断点。

如果您进行了适当的函数组合,那么在您的管道中插入一些 tap 调用是便宜、简单且非侵入性的,但它不会给您提供比断点更多的信息(并且知道如何使用调试器逐步执行您的代码)会。


x 上应用函数并 returning x 原样,无论如何,已经有了一个名称:tap。在这样的库中,描述如下:

Runs the given function with the supplied object, then returns the object.

由于 filtermap、...所有 return 个新实例,您可能别无选择,只能扩展原型。

不过,我们可以找到以 受控 方式进行的方法。这就是我的建议:

const debug = (xs) => {
  Array.prototype.tap = function (fn) {
    fn(this);
    return this;
  };
  
  Array.prototype.debugEnd = function () {
    delete Array.prototype.tap;
    delete Array.prototype.debugEnd;
    return this;
  };
 
  return xs;
};

const a = [1, 2, 3];

const b =
  debug(a)
    .tap(x => console.log('Step 1', x))
    .filter(x => x % 2 === 0)
    .tap(x => console.log('Step 2', x))
    .map(x => x * 10)
    .tap(x => console.log('Step 3', x))
    .debugEnd();

console.log(b);

try {
  b.tap(x => console.log('WAT?!'));
} catch (e) {
  console.log('Array prototype is "clean"');
}

如果您能负担得起像 Ramda 这样的库,最安全的方法(恕我直言)是在您的管道中引入 tap

const a = [1, 2, 3];

const transform =
  pipe(
      tap(x => console.log('Step 1', x))
    , filter(x => x % 2 === 0)
    , tap(x => console.log('Step 2', x))
    , map(x => x * 10)
    , tap(x => console.log('Step 2', x))
  );
  
console.log(transform(a));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {pipe, filter, map, tap} = R;</script>

如果您不想覆盖其中任何一个的原型,您可以编写一个包装函数,它接受一个承诺并为您提供一个修改后的承诺,该承诺具有您想要的附加功能。然而,这里的问题是你需要导入所有可能使用的方法,这对 tree-shaking 不利。

ES6 pipeline operator proposal 试图解决这个问题。

在那之前,像 lodashs _.flow 这样的东西仍然存在,允许你这样做:

_.pipe([
  _.filter(fn),
  _.filter(fn2),
])(data);

现在您基本上希望以异步方式进行此操作。使用像 Ramda 这样的工具应该很容易完成。

这里的主要问题是您正在尝试使用不能很好扩展的链接模式。

a.method().method() 只允许您应用给定上下文原型支持的函数(方法)(在本例中为 a)。

我宁愿建议你看一下函数组合(pipe VS compose)。此设计模式不依赖于特定上下文,因此您可以在外部提供行为。

const asyncPipe = R.pipeWith(R.then);

const fetchWarriors = (length) => Promise.resolve(
  Array.from({ length }, (_, n) => n),
);

const battle = asyncPipe([
  fetchWarriors,
  R.filter(n => n % 2 === 0),
  R.filter(n => n / 5 < 30),
  R.map(n => n ** n),
  R.take(4),
  R.tap(list => console.log('survivors are', list)),
]);

/* const survivors = await */ battle(100);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

正如您从上面的代码片段中看到的那样,Array 类型并不是真正需要实现所有内容...

您可以使用 rubico 一个功能性(编程)承诺库

轻松地做您想做的事
import { pipe, tap, map, filter, transform } from 'rubico'

const pipeline = pipe([
  getData,
  filter(fn),
  tap((result) => { log(result.count) }),
  filter(fn2),
  debugFn,
  filter(fn3),
  map(fn4),
])

您可以使用 rubico 的 transform

将上述管道用作转换器(目前没有 debugFn,因为我不确定它所做的事情的确切性质)
transform(pipeline, [])

您剩下一个基于转导的高效转化管道。