spebbe / dartz

Functional programming in Dart
MIT License
749 stars 60 forks source link

Feature request: Either as left and right #117

Open duck-dev-go opened 1 year ago

duck-dev-go commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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.