Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Aggregate Errors on Tasks? #222

Open
vladejs opened this issue Jul 18, 2019 · 8 comments
Open

How to Aggregate Errors on Tasks? #222

vladejs opened this issue Jul 18, 2019 · 8 comments

Comments

@vladejs
Copy link

vladejs commented Jul 18, 2019

This is a use case I haven't been able to solve.

I have 3 http requests, if one fails, I want, at least, the result of the other 2. Ideally, I would want to know what error happened.

I'm still using data.task from folktale 1, and this is how I do the requests, which fails fast (if one fails, I only get the first error with no result):

const mergeResults = data => logs => customer => ({ data, logs, customer })

liftA3(
  mergeResults,
  getDataFromAPI(userId),
  getLogs(userId),
  getCustomer(customerID)
).fork(
   error => ... Handle the first thrown error,
   result => ... All 3 requests succeeded here
)

Here, I would like to have data and logs returned if getCustomer API call fails.
Thanks in advance

@robotlolita
Copy link
Member

Sadly there's no simple way of doing this, but you can convert the Task<A, B> to a Task<void, Validation<A, B>> — where the task always "succeeds". Then you'll need a function that will aggregate the errors from Validation. This is easier since there's no concurrency involved, and Validation already has an applicative instance that does aggregation.

You'll also need control.async to run stuff in parallel.

For example:

const Task = require('data.task');
const Validation = require('data.either');
const { parallel } = require('control.async');


const toTaskOfResults = task =>
  task.fold(error => Task.of(Validation.Failure([error])),
            value => Task.of(Validation.Success(value)));

parallel([
  getDataFromAPI(userId),
  getLogs(userId),
  getCustomer(userId)
]).map(([a, b, c]) => liftA3(mergeResults, a, b, c))
  .fork(
    ...
  );

If you write a liftA that takes an array of things, and make it curried, you can simplify that to .map(liftA(mergeResults)) too.

@vladejs
Copy link
Author

vladejs commented Jul 19, 2019 via email

@robotlolita
Copy link
Member

robotlolita commented Jul 20, 2019

Oh, oops, this is what I get for not testing things. There's so much wrong with that example, sorry (I haven't touched v1 for a long time) :')

Anyway. My suggestion was that you transform all of your Task<Error, Result> to Task<void, Validation<Error, Result>>—so all of your tasks will be successful, in Task parlance, but they'll still carry the distinction between failure and success in its value. This is because Task was designed to be sequential, in the sense that if anything fails, then everything else should fail as well—it doesn't make sense to continue processing. Sometimes this assumption doesn't hold, but there aren't operations on Task that help with those edge cases yet, not even in Folktale 2.

You can construct such operation by combining Validations and control.async's parallel (or Folktale 2's Task.waitAll):

const Task = require('data.task');
const Validation = require('data.validation');
const { parallel } = require('control.async')(Task);

const toTaskOfValidations = (task) =>
  task.map(Validation.of)
      .orElse(error => Task.of(Validation.Failure([error])));

const liftA = (Applicative, f) => (applicatives) =>
  applicatives.reduce((a, b) => a.ap(b), Applicative.of(f));

const parallelCollectingFailures = (f, tasks) =>
  parallel(tasks.map(toTaskOfValidations))
    .map(liftA(Validation, f))
    .chain(validation => validation.fold(Task.rejected, Task.of));

And example usage would be like this:

const getDataFromAPI = a => Task.of('data');
const getLogs = b => Task.rejected('logs');
const getCustomer = c => Task.rejected('customer');

const mergeResults = data => logs => customer => ({ data, logs, customer });

const userId = 1;

parallelCollectingFailures(mergeResults, [
  getDataFromAPI(userId),
  getLogs(userId),
  getCustomer(userId)
]).fork(
    error => console.log('error', error),
    value => console.log('ok', value)
  );

Which should give you error [ 'logs', 'customer' ] when you run it.

@vladejs
Copy link
Author

vladejs commented Jul 20, 2019 via email

@diasbruno
Copy link
Contributor

diasbruno commented Jul 20, 2019

[Edit]
This is equivalent to toTaskOfResults as @robotlolita pointed out.

but you can convert the Task<A, B> to a Task<void, Validation<A, B>> — where the task always "succeeds"

Another option is to convert a rejected task into a resolved one.

// here you would have 
// f, g :: () -> Task () b
// but you can also use a `bimap` to make it something link
// f, g :: () -> Task () (Validation a b)
const validate = task => task.map(Success).orElse(compose(Task.of, Failure));
const f = () => validate(Task.of(1));
const g = () => validate(Task.rejected(2));


parallel([f(), g()]).fork(/* unused */, data => ...);

// data = [ folktale:Validation.Success({ value: 1 }),
//          folktale:Validation.Failure({ value: 2 }) ]

@robotlolita
Copy link
Member

Oh! So you want individual results rather than the overall results. That's easier.

With:

const Task = require('data.task');
const Validation = require('data.validation');
const { parallel } = require('control.async')(Task);

const toTaskOfValidations = (task) =>
  task.map(Validation.of)
      .orElse(error => Task.of(Validation.Failure(error)));

const runEach = (tasks) =>
  parallel(tasks.map(toTaskOfValidations));

And use it as:

const getDataFromAPI = a => Task.of('data');
const getLogs = b => Task.rejected('logs');
const getCustomer = c => Task.rejected('customer');

const mergeResults = (data, logs, customer) => ({ data, logs, customer });

const userId = 1;

runEach([
  getDataFromAPI(userId),
  getLogs(userId),
  getCustomer(userId)
]).map(xs => mergeResults(...xs))
  .fork(
    error => /* never happens */,
    value => console.log(value)
  );

Here each value of the results record will be a Validation, however, so you'll need to pattern match on whether each individual result is a failure or a success.

@vladejs
Copy link
Author

vladejs commented Jul 20, 2019 via email

@diasbruno
Copy link
Contributor

diasbruno commented Jul 20, 2019

Sorry for suddenly jump in. I've recently needed something like this, but with plain promises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants