FP-TS 分支(面向铁路的编程)
FP-TS Branching (Railway Oriented Programming)
我在尝试使用 FP-TS 实现事物时经常遇到的一种模式是,当我的管道涉及 TaskEither 的分支和合并分支时。
合并似乎工作得很好,因为我可以使用 sequenceT 创建数组并将它们通过管道传输到函数中,然后使用所有这些值。
似乎效果不佳的是更复杂的依赖关系图,其中一个函数需要较早的项目,然后需要该函数的输出以及第一个任务的原始结果。
基本上像这样的函数签名(这可能不是 100% 正确的类型,但要理解它的要点):
function fetchDataA(): TaskEither<Error, TypeA> {
}
function fetchBBasedOnOutputOfA(a: TypeA): TaskEither<Error, TypeB> {
}
function fetchCBasedOnOutputOfAandB(a: TypeA, b: TypeB): TaskEither<Error, TypeC> {
}
因为在管道中,你可以很好地为前两个作曲
pipe(
fetchDataA(),
TE.map(fetchBBasedOnOutputOfA)
)
这个管道 returns TaskEither 符合预期,地图处理错误对我来说很好。
而要执行最后一个操作,我现在需要输入 TypeA 作为参数,但它不可用,因为它已传递给 B。
一个解决方案是让函数 B 输出 A 和 B,但这感觉不对,因为创建 B 的函数不应该知道其他函数也需要 A。
另一种方法是创建某种中间函数来存储 A 的值,但在我看来,这打破了使用 TaskEither 的全部要点,对我来说,它抽象出所有错误类型并进行处理自动。
我会有一些奇怪的功能:
async function buildC(a : TypeA): TaskEither<Error, TypeC> {
const b = await fetchBBasedOnOutputOfA(a);
// NOW DO MY OWN ERROR HANDLING HERE :(
if (isRight(b)) {
return fetchCBasedOnOutputOfAandB(a, b);
}
// etc.
那么有没有更惯用的方法来做到这一点,也许创建树结构并遍历它们?老实说,Traverse 的文档中很少有代码示例,而且我不知道如何使用它们。
我想说有两种惯用的写法:
- 使用嵌套调用
chain
:
pipe(
fetchDataA(),
TE.chain(a => { // capture `a` here
return pipe(
fetchBBasedOnOutputOfA(a), // use `a` to get `b`
TE.chain(b => fetchCBasedOnOutputOfAandB(a, b)) // use `a` and `b` to get `c`
)
})
)
- 使用
Do
表示法:fp-ts
公开了一个“do”语法,可以减少与chain
的过度嵌套,尤其是当您需要捕获大量稍后重用的值时在程序流程的不同部分。
pipe(
// begin the `do` notation
TE.Do,
// bind the first result to a variable `a`
TE.bind('a', fetchDataA),
// use your `a` to get your second result, and bind that to the variable `b`
TE.bind('b', ({ a }) => fetchBBasedOnOutputOfA(a)),
// finally, use `a` and `b` to get your third result, and return it
TE.chain(({ a, b }) => fetchCBasedOnOutputOfAandB(a, b))
);
您可以查看 Do 表示法的语法 here。
我在尝试使用 FP-TS 实现事物时经常遇到的一种模式是,当我的管道涉及 TaskEither 的分支和合并分支时。
合并似乎工作得很好,因为我可以使用 sequenceT 创建数组并将它们通过管道传输到函数中,然后使用所有这些值。
似乎效果不佳的是更复杂的依赖关系图,其中一个函数需要较早的项目,然后需要该函数的输出以及第一个任务的原始结果。
基本上像这样的函数签名(这可能不是 100% 正确的类型,但要理解它的要点):
function fetchDataA(): TaskEither<Error, TypeA> {
}
function fetchBBasedOnOutputOfA(a: TypeA): TaskEither<Error, TypeB> {
}
function fetchCBasedOnOutputOfAandB(a: TypeA, b: TypeB): TaskEither<Error, TypeC> {
}
因为在管道中,你可以很好地为前两个作曲
pipe(
fetchDataA(),
TE.map(fetchBBasedOnOutputOfA)
)
这个管道 returns TaskEither
而要执行最后一个操作,我现在需要输入 TypeA 作为参数,但它不可用,因为它已传递给 B。
一个解决方案是让函数 B 输出 A 和 B,但这感觉不对,因为创建 B 的函数不应该知道其他函数也需要 A。
另一种方法是创建某种中间函数来存储 A 的值,但在我看来,这打破了使用 TaskEither 的全部要点,对我来说,它抽象出所有错误类型并进行处理自动。
我会有一些奇怪的功能:
async function buildC(a : TypeA): TaskEither<Error, TypeC> {
const b = await fetchBBasedOnOutputOfA(a);
// NOW DO MY OWN ERROR HANDLING HERE :(
if (isRight(b)) {
return fetchCBasedOnOutputOfAandB(a, b);
}
// etc.
那么有没有更惯用的方法来做到这一点,也许创建树结构并遍历它们?老实说,Traverse 的文档中很少有代码示例,而且我不知道如何使用它们。
我想说有两种惯用的写法:
- 使用嵌套调用
chain
:
pipe(
fetchDataA(),
TE.chain(a => { // capture `a` here
return pipe(
fetchBBasedOnOutputOfA(a), // use `a` to get `b`
TE.chain(b => fetchCBasedOnOutputOfAandB(a, b)) // use `a` and `b` to get `c`
)
})
)
- 使用
Do
表示法:fp-ts
公开了一个“do”语法,可以减少与chain
的过度嵌套,尤其是当您需要捕获大量稍后重用的值时在程序流程的不同部分。
pipe(
// begin the `do` notation
TE.Do,
// bind the first result to a variable `a`
TE.bind('a', fetchDataA),
// use your `a` to get your second result, and bind that to the variable `b`
TE.bind('b', ({ a }) => fetchBBasedOnOutputOfA(a)),
// finally, use `a` and `b` to get your third result, and return it
TE.chain(({ a, b }) => fetchCBasedOnOutputOfAandB(a, b))
);
您可以查看 Do 表示法的语法 here。