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

Feature request: Either as left and right #117

Open
duck-dev-go opened this issue Jan 4, 2023 · 8 comments
Open

Feature request: Either as left and right #117

duck-dev-go opened this issue Jan 4, 2023 · 8 comments

Comments

@duck-dev-go
Copy link

In the following scenario

https://stackoverflow.com/questions/75007868/with-dartz-is-it-possible-to-pass-a-failure-on-to-the-function-that-is-folding-m/75008089#75008089

it would be great to have an asLeft and asRight as was proposed in the answer.

Future<Either<Failure, AuthUser>> call() async {
    final userResponse = await _authRepository.getUser();

    if(userResponse.isLeft()){
       // do your stuffs       
     }

Inside this, you may like to access data directly, this extension will help

extension EitherX<L, R> on Either<L, R> {
  R asRight() => (this as Right).value; //
  L asLeft() => (this as Left).value;
}
@cranst0n
Copy link
Contributor

cranst0n commented Jan 5, 2023

I think the more idiomatic solution to the SO question would use map. Something like:

Future<Either<Failure, AuthUser>> call() async {
  final userResponse = await _authRepository.getUser();

  return userResponse.flatMap((user) {
    final accessTokenExpiresAtMilliseconds =
        user.accessTokenIssuedAtMilliseconds +
            user.accessTokenExpiresInMilliseconds;

    final accessTokenExpiresAtDateTime =
        DateTime.fromMillisecondsSinceEpoch(accessTokenExpiresAtMilliseconds);

    if (DateTime.now().isBefore(accessTokenExpiresAtDateTime)) {
      return right(user);
    }

    return _authRepository.refreshUser(user: user);
  });
}

This makes some assumptions on the signatures of the AuthRepository methods, but the general idea should hold. This uses the basic Either combinators and type system instead of having to rely on an error prone cast.

@duck-dev-go
Copy link
Author

duck-dev-go commented Jan 5, 2023

The map is what I used before but you can get deeply nested by doing that if the logic is just a bit more complex. Other than that I don't think there is an async map either so it doesn't seem to work well with async functions.

I understand that the either promotes handling all errors. But in certain situation I think you should be able to deviate from it so that the code doesn't become unnecessarily complex.

@cranst0n
Copy link
Contributor

cranst0n commented Jan 5, 2023

For async behavior, I would recommend using Task instead of Future. To combat complexity/nesting, break parts into smaller pieces and then use the existing combinators on Either, Task, etc. to build up your program to the desired functionality.

@duck-dev-go
Copy link
Author

duck-dev-go commented Jan 14, 2023

Is there any example how to use combinators? I'm assuming this would work also for 2 eithers the have different types for right? How would one combine 2 eithers and fold it?

@cranst0n
Copy link
Contributor

Sure a simple example:

final foo = right<String, int>(42);
final bar = right<String, String>("duck");

final combined = Either.map2(foo, bar, (a, b) => 'happy $a birthday $b!!');

final folded = combined.fold((l) => 'error: $l', (r) => r);

@duck-dev-go
Copy link
Author

duck-dev-go commented Apr 13, 2023

Hi thanks for the example. I'm still however a bit confused. In my example I have 2 futures that I work with. The first one needs to resolve because I need it's result for the second one. Your example is not for async operations it seems. So you suggest using a Task right? I'm not seeing myself how a task would solve this problem.

So the code below would work with the extension. But how can I make this same code work with the api provided by this package? A map won't work since we are working with a future here.

Future<Either<Failure, SomeValue1>> doSomething1() { 
    // return left and right 
}

Future<Either<Failure, SomeValue2>> doSomething2(SomeValue1 input) { 
    // return left and right
}

Future<Either<Failure, SomeValue2>> combine() {
    final result = await doSomething1();
    
    if(result.isLeft()){
        return result.asLeft();
    }
    
    return doSomething2(result.asRight());
}

@cranst0n
Copy link
Contributor

Yes you're correct that in your case Task alone won't solve your problem. It can get a tad more messy when you're working with nested Monads, Task and Either in this case, but this pattern should solve your issue:

// Your original Future-based functions
Future<Either<Failure, SomeValue1>> doSomething1Fut() =>
    throw UnimplementedError();

Future<Either<Failure, SomeValue2>> doSomething2Fut() =>
    throw UnimplementedError();

// Dartz Task functions
Task<Either<Failure, SomeValue1>> doSomething1() =>
    Task(() => doSomething1Fut());

Task<Either<Failure, SomeValue2>> doSomething2() =>
    Task(() => doSomething2Fut());

// Using flatMap to sequence the Tasks, map2 to combine the Eithers
Task<Either<Failure, SomeValue3>> combine() {
  return doSomething1().flatMap((value1) {
    return doSomething2().map((value2) {
      return Either.map2(value1, value2,
          (a, b) => throw UnimplementedError('combine the values here'));
    });
  });
}

@duck-dev-go
Copy link
Author

For me a scenario like this comes up a lot. For example when you are working with remote and local datasources and you want to write to the cache or disk in-between. I can imagine that the solution I proposed doesn't align with the goals of this project. But I do think ideally there should be a more streamlined solution for this supported by the package itself.

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

No branches or pull requests

2 participants