[英]How can I avoid the pyramid of doom with chain in fp-ts?
I frequently come up against this situation, where I need to complete several sequential operations.我经常遇到这种情况,我需要完成几个顺序操作。 If each operation exclusively used data from the previous step, then I could happily do something like
pipe(startingData, TE.chain(op1), TE.chain(op2), TE.chain(op3), ...)
.如果每个操作都只使用上一步中的数据,那么我可以很高兴地执行诸如
pipe(startingData, TE.chain(op1), TE.chain(op2), TE.chain(op3), ...)
。 I can't find a nice way to write this when op2
also needs data from startingData
, without a bunch of nested callbacks.当
op2
也需要来自startingData
数据而没有一堆嵌套回调时,我找不到写这个的好方法。
How can I avoid the pyramid of doom in the example below?如何避免下面示例中的厄运金字塔?
declare const op1: (x: {one: string}) => TE.TaskEither<Error, string>;
declare const op2: (x: {one: string, two: string}) => TE.TaskEither<Error, string>;
declare const op3: (x: {one: string, two: string, three: string}) => TE.TaskEither<Error, string>;
pipe(
TE.of<Error, string>('one'),
TE.chain((one) =>
pipe(
op1({ one }),
TE.chain((two) =>
pipe(
op2({ one, two }),
TE.chain((three) => op3({ one, two, three }))
)
)
)
)
);
There is a solution to the problem, and it's called "do notation".还有就是要解决问题的办法,这就是所谓的“做记号”。 It's been available in
fp-ts-contrib
for a while, but it now also has a version baked into fp-ts
itself using the bind
function (which is defined on all monadic types).它已经在
fp-ts-contrib
可用一段时间了,但现在它也有一个版本,它使用bind
函数(在所有 monadic 类型上定义)烘焙到fp-ts
本身中。 The basic idea is similar to what I was doing below - we bind computation results to a particular name, and track those names inside a "context" object as we go along.基本思想类似于我在下面所做的 - 我们将计算结果绑定到一个特定的名称,并在我们进行时在“上下文”对象中跟踪这些名称。 Here's the code:
这是代码:
pipe(
TE.of<Error, string>('one'),
TE.bindTo('one'), // start with a simple struct {one: 'one'}
TE.bind('two', op1), // the payload now contains `one`
TE.bind('three', op2), // the payload now contains `one` and `two`
TE.bind('four', op3), // the payload now contains `one` and `two` and `three`
TE.map(x => x.four) // we can discharge the payload at any time
)
I've come up with a solution that I'm not very proud of, but I'm sharing it for possible feedback!我想出了一个我并不引以为豪的解决方案,但我正在分享它以获取可能的反馈!
Firstly, define some helper functions:首先,定义一些辅助函数:
function mapS<I, O>(f: (i: I) => O) {
return <R extends { [k: string]: I }>(vals: R) =>
Object.fromEntries(Object.entries(vals).map(([k, v]) => [k, f(v)])) as {
[k in keyof R]: O;
};
}
const TEofStruct = <R extends { [k: string]: any }>(x: R) =>
mapS(TE.of)(x) as { [K in keyof R]: TE.TaskEither<unknown, R[K]> };
mapS
allows me to apply a function to all values in an object (subquestion 1: is there a builtin function that would allow me to do this?). mapS
允许我将函数应用于对象中的所有值(子问题 1:是否有内置函数可以让我这样做?)。 TEofStruct
uses this function to turn a struct of values into a struct of TaskEither
s for those values. TEofStruct
使用此函数将值结构TaskEither
为这些值的TaskEither
结构。
My basic idea is to accumulate the new value along with the previous values using TEofStruct
and sequenceS
.我的基本想法是使用
TEofStruct
和sequenceS
将新值与以前的值一起累积。 So far it looks like this:到目前为止它看起来像这样:
pipe(
TE.of({
one: 'one',
}),
TE.chain((x) =>
sequenceTE({
two: op1(x),
...TEofStruct(x),
})
),
TE.chain((x) =>
sequenceTE({
three: op2(x),
...TEofStruct(x),
})
),
TE.chain((x) =>
sequenceTE({
four: op3(x),
...TEofStruct(x),
})
)
);
It feels like I could write some kind of helper function that combines sequenceTE
with TEofStruct
to reduce the boilerplate here, but I'm still not sure overall if this is the right approach, or if there is a more idiomatic pattern!感觉我可以编写某种辅助函数,将
sequenceTE
与TEofStruct
结合起来以减少这里的样板文件,但我仍然不确定总体上这是否是正确的方法,或者是否有更惯用的模式!
Official fp-ts DO notation is here: https://gcanti.github.io/fp-ts/guides/do-notation.html官方 fp-ts DO 符号在这里: https : //gcanti.github.io/fp-ts/guides/do-notation.html
But there is nothing wrong with nested logic extraction to its own function.但是嵌套逻辑提取到它自己的函数并没有错。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.