函数式编程 - 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
。在ramda.js这样的库中,描述如下:
Runs the given function with the supplied object, then returns the object.
由于 filter
、map
、...所有 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, [])
您剩下一个基于转导的高效转化管道。
我正在这样解析数据:
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()
个调用链中,如您所述。
我相信可以使用断点来调试这样的东西。
编辑
经过一番思考,我确信
如果您进行了适当的函数组合,那么在您的管道中插入一些 tap
调用是便宜、简单且非侵入性的,但它不会给您提供比断点更多的信息(并且知道如何使用调试器逐步执行您的代码)会。
在 x
上应用函数并 returning x
原样,无论如何,已经有了一个名称:tap
。在ramda.js这样的库中,描述如下:
Runs the given function with the supplied object, then returns the object.
由于 filter
、map
、...所有 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
transform(pipeline, [])
您剩下一个基于转导的高效转化管道。