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

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

Closed
konstantin-doncov opened this issue Feb 6, 2020 · 15 comments
Closed

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

konstantin-doncov opened this issue Feb 6, 2020 · 15 comments

Comments

@konstantin-doncov
Copy link

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
Copy link

torbenkeller commented Feb 6, 2020

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
Copy link
Author

@torbenkeller thanks!

@konstantin-doncov
Copy link
Author

@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
Copy link

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
Copy link
Author

konstantin-doncov commented Feb 16, 2020

@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
Copy link

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
Copy link
Author

@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
Copy link

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
Copy link
Author

@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
Copy link

ResoDev commented Mar 1, 2020

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
Copy link
Owner

spebbe commented Mar 3, 2020

@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 spebbe closed this as completed Mar 3, 2020
@spebbe
Copy link
Owner

spebbe commented Mar 3, 2020

@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
Copy link

lts1610 commented Mar 23, 2020

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
Copy link

e200 commented Sep 15, 2020

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

@sahildev001
Copy link

sahildev001 commented Jan 24, 2022

@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.

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

7 participants