spebbe / dartz

Functional programming in Dart
MIT License
755 stars 59 forks source link

Use Either with then() and catchError() #34

Closed konstantin-doncov closed 4 years ago

konstantin-doncov commented 4 years ago

I want to use Either in this way:

Either<Failure, MyResponse> result = await restClient.request() // Future<MyResponse> request();
    .then((response) => Right(response))
    .catchError((failure) => Left(Failure()));

But it seems that I can't do this:

error: A value of type 'Right< dynamic, MyResponse>' can't be assigned to a variable of type 'Either< Failure, MyResponse>'.

I necessarily need to use explicit type casting, like this:

Either<Failure, MyResponse> result = await restClient.request() // Future<MyResponse> request();
    .then((response) => Right(response))
    .catchError((failure) => Left(Failure())) as Either<Failure, MyResponse>; 

even if I already return Left(Failure()) in the catchError(). So, is explicit casting the most concise and elegant solution?

torbenkeller commented 4 years ago

You just forgot to type Right and Left. Try this:

Either<Failure, MyResponse> result = await restClient.request() // Future<MyResponse> request();
    .then((response) => Right<Failure, MyResponse>(response))
    .catchError((failure) => Left<Failure, MyResponse>(Failure()));
konstantin-doncov commented 4 years ago

@torbenkeller thanks!

konstantin-doncov commented 4 years ago

@torbenkeller it's really strange, but I'm trapped in the similar situation :). Now I have a runtime exeption:

type 'Left<Failure, MyResponse>' is not a subtype of type 'FutureOr<Right<Failure, MyResponse>>'

When your code goes into cathcError(...). How can I fix it?

torbenkeller commented 4 years ago

I don't know exactly why converting results of Futures to Eithers doesnt work as expected but there is the Task Object in the package which is solving this for you. Try this:

Task<MyResponse>(() => restClient.request())
  .attempt() //returns Task<Either<Object, MyResponse>>
  //Now you have to convert Task<Either<Object, MyResponse>> to Task<Either<Failure, MyResponse>>
  .map(
    (Either<Object, MyResponse> either) => either.leftMap((Object obj) {
      try {
        return obj as Failure;
      } catch (e) {
        throw obj;
      }
    }),
  )
  // Now you have to convert it to a Future<Either<Failure, MyResponse>> again
  .run();

But this map function is boilerplate code so you can outsource it to an extension. Try this:

extension TaskX<T extends Either<Object, dynamic>> on Task<T> {
  Task<Either<Failure, A>> mapLeftToFailure<A>() {
    return map<Either<Failure, A>>((Either<Object, dynamic> either) =>
        either.fold<Either<Failure, A>>((Object obj) {
          try {
            return Left<Failure, A>(obj as Failure);
          } catch (e) {
            throw obj;
          }
        }, (dynamic u) {
          try {
            return Right<Failure, A>(u as A);
          } catch (e) {
            throw u;
          }
        }));
  }
}

You can use it like this:

import'###extension.dart###';

...

Task<MyResponse>(() => restClient.request())
  .attempt()
  .mapLeftToFailure<MyResponse>()
  .run();
konstantin-doncov commented 4 years ago

@torbenkeller many thanks for this extension! I generalized it a bit for my needs:

extension TaskX<T extends Either<Object, dynamic>> on Task<T> {

  Task<Either<Failure, A>> mapLeftToFailure<A>({@required Function onLeft, @required Function onRight}) {
    return map<Either<Failure, A>>((Either<Object, dynamic> either) =>
        either.fold<Either<Failure, A>>((Object obj) {
          try {
            return onLeft(obj);
          } catch (e) {
            throw obj;
          }
        }, (dynamic u) {
          try {
            return onRight(u as A);
          } catch (e) {
            throw u;
          }
        }));
  }
}

So now I can do this:

Task<MyResponse>(() => restClient.request())
  .attempt()
  .mapLeftToFailure<MyResponse>(
        onLeft: (error) {
          //do something and return Left
        },
        onRight: (response) {
          //do something and return Right
        })
  .run();

But let's suppose I want to call and return in onLeft some nested function which returns Future<Either<Failure, MyResponse>>, if so then I need to await:

        onLeft: (error) async {
         return await requestOneMoreTime();
        },

But now I also need to await in the my extension:

return await onLeft(obj);

So, I need to mark it as async and so on... But I don't know how to do it correctly and elegantly with mapLeftToFailure() and Task<MyResponse>. Please, can you help me with this?

torbenkeller commented 4 years ago

I would not recommend changing the extension. It is against the idea why you want to use an Either<Failure, MyResponse>.

The Idea is you have runtime exceptions and specific cases like "No Data Selected" and create Failures from it. You use Failures to be independent of the "data selection layer". So when you create the Either the "data selection layer" is already throwing Failures and not HTTP Response Exceptions for example. And when you create the Either you don't want to be any buisness logic there.

In code it would be like this:

import 'package:http/http.dart' as http;

class RestClient {

  ...

  Future<MyResponse> request() {
    final Response response = await http.get(...);
    switch (response.statusCode) {
      case 200:
        return MyResponse(response.body);
      case 404:
        throw ...Failure();
      ...
    }
    throw Failure();
  }
}
konstantin-doncov commented 4 years ago

@torbenkeller you are right. But my restClient.request() is generated by the Retrofit method, so I can't do anything inside this method. It returns response or DioError which I need to handle(as your switch inside request()), so I created a function Future<Either<Failure, dynamic>> _processNetworkErrors(dynamic error) which I want to call inside onLeft.

torbenkeller commented 4 years ago

Then write a wrapper function around the request function like this:

Future<MyResponse> requestWrapper() async {
  try {
    return await http.get(...);
  } on ...Exception {
    throw ...Failure();
  }
}
konstantin-doncov commented 4 years ago

@torbenkeller unfortunately, I didn't get your whole idea.

I have RemoteDataSource with Retrofit Api:

abstract class RemoteDataSource {
...
  @GET("/request")
  Future<MyResponse> request(); //only declaration 
...
}

And NetworkRepository:

class NetworkRepository{
...
Future<Either<Failure, MyResponse>> request([bool isNeedToRecall = true]) async {

Either<Failure, MyResponse> failOrResult = await _callAndProcessErrors<MyResponse>(
        functionToRecall: isNeedToRecall ? ()=> request(false)  : null,
        request: () => remoteDataSource.request());

    return failOrResult;
}

  Future<Either<Failure, A>> _callAndProcessErrors<A>({ @required Future<A> request(), @required Future<Either<Failure, dynamic>> functionToRecall()}) async {

    Either<dynamic, A> errorOrResult =  await Task<A>(() => request())
        .attempt()
        .mapLeftAndRight<dynamic, A>(
        onLeft: (error) => Left<dynamic,A>(error),
        onRight: (response) => Right<dynamic,A>(response))
        .run();

    return errorOrResult.fold(
            (error) async {
          return await _processNetworkErrors(error, functionToRecall);
        },
            (result) => Right<Failure, A>(result));
  }

  Future<Either<Failure, dynamic>> _processNetworkErrors(dynamic error, Future<Either<Failure, dynamic>> functionToRecall()) async {
    if(error is DioError){
      switch(error.response.statusCode){
        case 401:
          //unauthorized
          //so let's try to refreshToken() 
          //and if it's ok and functionToRecall != null
          return functionToRecall() ?? Left<Failure, dynamic>(...) ;//recall the first request() one more time(but no more)
        case 403:
        ...
      }
    }
    return Left<Failure, dynamic>(...);
  }
}

Here is an example of the case when I call the request, get 401, refresh access token, and call the first request again.

How do you propose to do this and similar tasks?

ResoDev commented 4 years ago

Hello, I didn't read through the whole conversation here but I think you may find these extension methods useful. Since I'm not an FP pro by any means, they're probably badly named but they serve the purpose well. Basically, it's an EitherT type specifically for Futures.

import 'package:dartz/dartz.dart';

extension FutureEither<L, R> on Future<Either<L, R>> {
  Future<Either<L, R2>> flatMap<R2>(Function1<R, Future<Either<L, R2>>> f) {
    return then(
      (either1) => either1.fold(
        (l) => Future.value(left<L, R2>(l)),
        f,
      ),
    );
  }

  Future<Either<L, R2>> map<R2>(Function1<R, Either<L, R2>> f) {
    return then(
      (either1) => either1.fold(
        (l) => Future.value(left<L, R2>(l)),
        (r) => Future.value(f(r)),
      ),
    );
  }

  // TODO: Find an official FP name for mapping multiple layers deep into a nested composition
  Future<Either<L, R2>> nestedMap<R2>(Function1<R, R2> f) {
    return then(
      (either1) => either1.fold(
        (l) => Future.value(left<L, R2>(l)),
        (r) => Future.value(right<L, R2>(f(r))),
      ),
    );
  }

  Future<Either<L2, R>> leftMap<L2>(Function1<L, L2> f) {
    return then(
      (either1) => either1.fold(
        (l) => Future.value(left(f(l))),
        (r) => Future.value(right<L2, R>(r)),
      ),
    );
  }
}

These allow you to flatMap, map, leftMap and "nestedMap" (over the inner Either) with ease. Then, just await the whole expression.

spebbe commented 4 years ago

@ResoDev, great stuff! Thank you for helping! This approach to getting some of the functionality of monad transformers looks really promising -- i'll probably take some inspiration from this once I'm able to drop Dart 1 support :-)

Thank you also @torbenkeller for helping out!

I'll close this ticket for now, since it looks a bit too open ended, but feel free to re-open if you feel the need.

spebbe commented 4 years ago

@ResoDev: Oh, and if one chooses to view your extensions as forming a composed monad, it is probably the current map that should be renamed (attemptMap, maybe?). The current nestedMap corresponds more closely to map from a Functor law perspective.

lts1610 commented 4 years ago

Inspired by ResoDev, I wrote something like this Future Option:

import 'package:dartz/dartz.dart';

extension FutureOption<A> on Future<Option<A>> {
  Future<Option<B>> flatMap<B>(Function1<A, Future<Option<B>>> f) {
    return then(
      (option) => option.fold(
        () => Future.value(none<B>()),
        f,
      ),
    );
  }

  Future<Option<B>> attemptMap<B>(Function1<A, Option<B>> f) {
    return then(
      (option) => option.fold(
        () => Future.value(none<B>()),
        (r) => Future.value(f(r)),
      ),
    );
  }

  Future<Option<B>> nestedMap<B>(Function1<A, B> f) {
    return then(
      (option) => option.fold(
        () => Future.value(none<B>()),
        (r) => Future.value(some(f(r))),
      ),
    );
  }

  Future<Either<L2, A>> toEither<L2>(L2 l) {
    return then(
      (option) => option.fold(
        () => Future.value(left<L2, A>(l)),
        (r) => Future.value(right<L2, A>(r)),
      ),
    );
  }
}
e200 commented 4 years ago

Why we can't have this on docs? Actually, this package have any docs somewhere?

sahildev001 commented 2 years ago

@POST("apiPrefix") Future initLoginApi(@Body() Map<String , dynamic> field);

_client.initLoginApi(fields).then((value) async { LoginModel model = value; if(model.statusCode == 200) { view.onSuccessSignIn(value); final prefs = await SharedPreferences.getInstance(); prefs.setString('accessToken', model.data?.access_token ?? ""); prefs.setString('driverId', model.data?.dataData?.id ?? ""); prefs.setString('driverName', '${model.data?.dataData?.Name} ${model.data?.dataData?.lastName}' ?? ""); prefs.setString('driverEmail', value.data?.dataData?.email ?? "");

  }else if(model.statusCode == 400){
    print("$tag fdsjlfjsdlkfj");
    print("$tag handle response:--- ${value.message} ");
    view.onError("catch error print");
  }
}).catchError((Object obj) {
  switch (obj.runtimeType) {
    case DioError:
      final res = (obj as DioError).response;
      print("Got error : ${res?.statusCode} -> ${res?.statusMessage}");
      print("dioerror :-- ${res?.data["message"]}");
      break;
    default:
      break;
  }
});

flutter catachError not working.