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

Evaluation usage #47

Closed
spebbe opened this issue May 29, 2020 · 5 comments
Closed

Evaluation usage #47

spebbe opened this issue May 29, 2020 · 5 comments
Assignees

Comments

@spebbe
Copy link
Owner

spebbe commented May 29, 2020

Hi! I've been messing around with EvaluationMonad and Evaluation for a few days now. One thing I just can't seem to figure out:

In my code, it's obviously common that I have a function that returns a Future<Either<MyException, MyValue>>. I can understand the use an Evaluation in the case where you have an Either<MyException, MyValue> (just M.liftEither!), or in the case where you have a Future<MyValue> (just M.liftFuture!), but I'm really having a hard time understanding how you can make use of it in the case were you're dealing with a Future<Either<A, B>>. I'm guessing I would need to refactor those functions that return Future, to return Future and somehow use the EvaluationMonad to set the E value on the Monad?

I looked through evaluation_test.dart in the repository, and there's a fairly clear example of liftFuture, and a fairly clear example of liftEither. But they're both completely distinct which makes me think there's no way out of the corner I've painted myself into? :)

Originally posted by @ibcoleman in #36 (comment)

@spebbe
Copy link
Owner Author

spebbe commented May 29, 2020

Hi @ibcoleman – don't worry, you haven't painted yourself into a corner!

I hope you don't mind that I created a new issue for this. Hopefully later today I'll have the time to put together some examples of how to deal with the situation you are in and post them here!

@spebbe spebbe self-assigned this May 29, 2020
@spebbe
Copy link
Owner Author

spebbe commented May 29, 2020

Hi again @ibcoleman!

Here's a small example that maybe helps you achieve what you want?:

abstract class Failure {}

class PointlessDepartment extends Failure {}

class UnexpectedFailure extends Failure {
  final Object cause;

  UnexpectedFailure(this.cause);
}

Future<Either<Failure, IList<int>>> getEmployeeIds(String departmentName) async {
  final lowercaseDepartmentName = departmentName.toLowerCase(); // I sure hope departmentName isn't null!
  if (lowercaseDepartmentName == "r&d") {
    return right(ilist([1, 2, 3]));
  } else {
    return left(PointlessDepartment());
  }
}

final M = EvaluationMonad<Failure, Unit, Unit, Unit>(UnitMi);

Evaluation<Failure, Unit, Unit, Unit, A> liftFE<A>(Future<Either<Failure, A>> fea) => M
  .liftFuture(fea.catchError((cause) => left<Failure, A>(UnexpectedFailure(cause))))
  .bind(M.liftEither);

void main() async {
  final successful = await liftFE(getEmployeeIds("R&D")).value(unit, unit);
  final expectedFailure = await liftFE(getEmployeeIds("HR")).value(unit, unit);
  final unexpectedFailure = await liftFE(getEmployeeIds(null)).value(unit, unit);

  assert(successful == right(ilist([1,2,3])));
  assert(expectedFailure.fold((err) => err is PointlessDepartment, (_) => false));
  assert(unexpectedFailure.fold((err) => err is UnexpectedFailure && err.cause is NoSuchMethodError, (_) => false));
}

The catchError + UnexpectedFailure deal is optional of course. You might already have a similar mechanism in lower layers that guarantees that no failed Futures will be returned, but it can be a nice extra safeguard.

Hope that helps!

@ibcoleman
Copy link

Thanks so much @spebbe! I'm going to go away and ponder this... :)

@spebbe
Copy link
Owner Author

spebbe commented Jun 18, 2020

Closing this for now – feel free to open again if needed!

@spebbe spebbe closed this as completed Jun 18, 2020
@ibcoleman
Copy link

I've been playing with EvaluationMonad over time, and really finding it cleans things up considerably. Based on your invaluable responses, I'm basically composing my functions that return Either or Future as follows:

  final M = EvaluationMonad<TheMobileException, Unit, Unit, Unit>(UnitMi); 
  final reqHeaders = M.liftEither(svc.getRequestHeaders());
  final getResponse = (headers) => M.liftFuture(getWithHeaders(targetUri, svc, prepareGetRequest(targetUri, headers)));

  return (await reqHeaders.bind((headers) => getResponse(headers))
      .value(unit, unit))
      .flatMap(id)
      .map((jsonString) => stringToJson(jsonString));

One thing I haven't quite figured out is how to "add" error handling to an either. So in the case above, String stringToJson(String jsonString) can blow up during parsing. While digging around I saw there's an Either<dynamic, A> catching<A>(Function0<A> thunk) in either.dart, but I don't see how one would use it.

Obivously the following is kind of screwed up, but I'm taking the String -> String function stringToJson and then lifting it to Either<TheMobileException, String>. Then I created an Evaluation for that so I could add it as another bind() in the monad evaluation chain. Then I added a trivial handleError() handler, but it doesn't seem to do anything when the stringToJson function blows up. I guess I'm going to have to write an "Either aware" version of stringToJson? Or is there some way to intercept that exception and convert it to an Either?

  test('Compose Eithers With Error Handling', () async {

    d.Either<TheMobileException, Map> getRequestHeaders() {
      return d.right({});
    }
    Future<d.Either<TheMobileException, String>> getWithHeaders(String u, String s, Map m) {
      return Future.value(d.right('A String'));
    }
    String stringToJson(String s) {
      throw Exception("Oh No!");
    }

    final M = EvaluationMonad<TheMobileException, Unit, Unit, Unit>(UnitMi);

    final reqHeaders = M.liftEither(getRequestHeaders());
    final getResponse = (headers) => M.liftFuture(getWithHeaders('', '', headers));
    final getStringToJsonAsEither = Either.lift<TheMobileException, String, String>(stringToJson);

    final getJsonString = (str) => M.liftEither((getStringToJsonAsEither(str)));
    final handleErrorE = (err) => M.liftEither(left(err));

    Either<TheMobileException, String> stringEither = (await reqHeaders
        .bind((headers) => getResponse(headers))
        .bind((response) => getJsonString(response))
        .handleError((err) => handleErrorE(err)) //  <= This doesn't seem to do anything
        .value(unit, unit));

    expect(stringEither.getOrElse(() => ""), equals("A String"));

  });

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