spebbe / dartz

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

Preserve typing on Task extensions #40

Closed narcodico closed 4 years ago

narcodico commented 4 years ago

I've made a couple of extension methods trying to simplify the extremely verbose syntax when using Task. I'm left mapping in case of a failure to preserve unexpected errors.

class AppFailure {}

extension TaskEitherExtensions<T extends Either<Object, U>, U> on Task<T> {
  Task<Either<AppFailure, U>> mapLeftToFailure() {
    return map<Either<AppFailure, U>>(
      (either) => either.leftMap<AppFailure>((error) {
        if (error is AppFailure) return error;
        throw error;
      }),
    );
  }
}

extension TaskExtensions<T, U> on Task<T> {
  Future<Either<AppFailure, U>> execute<U>() =>
      attempt().mapLeftToFailure().run();
}

And I wanna call it like:

Future<Either<AppFailure, Unit>> someMethod() {
        return Task(someFutureReturningVoid).execute<Unit>();
  }

However, I get type 'Future<Either<AppFailure, dynamic>>' is not a subtype of type 'Future<Either<AppFailure, Unit>>' I might be missing something here but is there a way of preserving type safety with my approach?

mateusfccp commented 4 years ago

Hi, @RollyPeres .

Your problem seems to be related to type inferentece and, thus, not related to dartz itself. Please, refer to #33 and #38, they are similar issues.

The problem probably will be solved simply by typecasting in the right place.

As I don't have the time to test your specific code right now, I may be wrong, so please feel free to tell me if this is not the case.

narcodico commented 4 years ago

Hi @mateusfccp , thanks for suggestions. It clearly has something to do with type inference but couldn't find a solution. Also attempt is not generic as suggested in #33 ...I'm using dartz: ^0.9.0-dev.5.

mateusfccp commented 4 years ago

Can you provide a minimal reproducible example?

narcodico commented 4 years ago

@mateusfccp There you go: https://gist.github.com/RollyPeres/72c3d80e39bf032209c5b2aaea7b42e7

mateusfccp commented 4 years ago

Well, your case is a little more complex. It seems Dart can't handle the "complex" case of T extends Either<Object, U> and can't infer U.

Let me ask: why have T to extend Either,<Object, U>? In the MRE you provided, you are not using any extension of Either, but Either itself.

I managed to run your example by replacing every occurrence of T with it's meaning:

extension TaskEitherExtensions<U> on Task<Either<Object, U>> {
  Task<Either<AppFailure, U>> mapLeftToFailure() {
    return map<Either<AppFailure, U>>(
      (Either<Object, U> either) => either.leftMap<AppFailure>((error) {
        if (error is AppFailure) return error;
        // simplified code for brevity...
        return AppFailure();
      }),
    );
  }
}

Of course, this will cause another problem. You are returning a Future<String> on something when doSomething expects Either<AppFailure, Unit>. Changing Unit to String will solve this:

Future<Either<AppFailure, String>> doSomething() {
  return Task(something).attempt().mapLeftToFailure().run();
}

Is this the expected behavior or am I missing something?

narcodico commented 4 years ago

That was the problem. I've experimented with a couple of approaches, so apparently I ended up not paying attention to basic stuff :)

My end goal was to have:

extension TaskExtensions<T> on Task<T> {
  Future<Either<AppFailure, T>> execute() => attempt().mapLeftToFailure().run();
}

This will greatly simplify all the boiler plate. Thanks @mateusfccp for having a fresh look at this!