typescript - FP-TS Ramificação (Programação Orientada a Ferrovia)
Um padrão que continuo encontrando ao tentar implementar coisas usando FP-TS é quando tenho encadeamentos que envolvem ramificação e a fusão de ramos de TaskEither juntos.
A fusão parece funcionar muito bem, porque posso usar sequenceT para criar arrays e passá-los para funções que então consomem todos esses valores.
O que parece não funcionar bem são gráficos de dependência mais complexos, onde um item anterior é necessário em uma função, e então a saída dessa função é requerida juntamente com o resultado original da primeira tarefa.
Basicamente assinaturas de funções assim (isso pode não estar 100% correto em relação aos tipos, mas pegue a ideia):
function fetchDataA(): TaskEither<Error, TypeA> {
}
function fetchBBasedOnOutputOfA(a: TypeA): TaskEither<Error, TypeB> {
}
function fetchCBasedOnOutputOfAandB(a: TypeA, b: TypeB): TaskEither<Error, TypeC> {
}
Porque no tubo, você pode compor bem para os dois primeiros
pipe(
fetchDataA(),
TE.map(fetchBBasedOnOutputOfA)
)
E este pipe retorna TaskEither
Enquanto para executar a última ação, eu preciso agora inserir TypeA como argumento, mas ele não está disponível pois já foi passado para B.
Uma solução é que a função B produza tanto A quanto B, mas isso parece errado, porque a função que cria B não deveria ter que saber que alguma outra função também precisa de A.
Outra é criar algum tipo de função intermediária para armazenar o valor de A, mas isso me parece quebrar todo o propósito de usar TaskEither, que para mim é abstrair todos os tipos de Erro e ter isso tratado automaticamente.
Eu teria algum tipo de função estranha:
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.
Então, existe uma maneira mais idiomática de fazer isso, talvez criando estruturas de árvore e percorrendo-as? Embora, para ser honesto, a documentação do Traverse seja bem escassa em exemplos de código e eu não tenha ideia de como usá-los.
Resposta #1
Eu diria que há duas maneiras idiomáticas de escrever isso:
- Usando chamadas aninhadas para
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`
)
})
)
- Usando a notação
Do:fp-tsexpõe uma sintaxe "do" que pode aliviar o excesso de aninhamento comchain, especialmente quando você precisa capturar muitos valores que são reutilizados posteriormente em diferentes partes do fluxo do seu programa.
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))
);
Você pode conferir a sintaxe da notação Do aqui.
