Home > other >  FP-TS Branching (Railway Oriented Programming)
FP-TS Branching (Railway Oriented Programming)

Time:10-15

A pattern I keep encountering trying to implement things using FP-TS is when I have pipes that involve branching and merging branches of TaskEither together.

Merging seems to work quite well, because I can use sequenceT to create arrays and pipe them into functions that then consume all those values.

What doesn't seem to work well is more complex dependency graphs, where an earlier item is needed in one function, then the output of that function is required along with the original result of the first task.

Basically function signatures like this (this may not be 100% correct types but get the jist of it):

function fetchDataA(): TaskEither<Error, TypeA> {
}

function fetchBBasedOnOutputOfA(a: TypeA): TaskEither<Error, TypeB> {
}

function fetchCBasedOnOutputOfAandB(a: TypeA, b: TypeB): TaskEither<Error, TypeC> {
}

Because in the pipe, you can compose nicely for the first two

pipe(
  fetchDataA(),
  TE.map(fetchBBasedOnOutputOfA)
)

And this pipe returns TaskEither<Error, TypeB> as expected, with map handling errors nicely for me.

Whereas to perform the last action, I need to now input TypeA as the argument, but it's not available as it has been passed into B.

One solution is for function B to output both A and B, but that feels wrong, because the function that creates B should not have to know that some other function also requires A.

Another is to create some kind of intermediate function to store the value of A, but that seems to me to break the entire point of using TaskEither, which is for me to abstract away all the Error types and have that handled automatically.

I'd have some kind of weird function:

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.

So is there a more idiomatic way to do this, perhaps creating tree structures and traversing them? Although to be honest the docs for Traverse are pretty sparse with code examples and I've got no idea how to use them.

CodePudding user response:

I'd say there's two idiomatic ways of writing this:

  1. Using nested calls to 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. Using Do notation: fp-ts exposes a "do" syntax that can alleviate excessive nesting with chain, especially when you need to capture a lot of values that are reused later in different parts of your program flow.
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))
);

You can check out the syntax for Do notation here.

  • Related