Pesquisa de site

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 como esperado, com o map lidando bem com os erros para mim.

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:

  1. 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`
    )
  })
)
  1. Usando a notação Do: fp-ts expõe uma sintaxe "do" que pode aliviar o excesso de aninhamento com chain, 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.

Artigos relacionados