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

Fold Either for a list of items #69

Closed
saltedpotatos opened this issue Oct 29, 2020 · 3 comments
Closed

Fold Either for a list of items #69

saltedpotatos opened this issue Oct 29, 2020 · 3 comments

Comments

@saltedpotatos
Copy link

I've recently started using Either types, or any functional programming, and am stuck trying to figure out how to yield a state in my bloc when I need to perform multiple actions in an event, and call a function on each item in a list.

Everything works well if I'm performing a single action like

final someActionOrFailure = await _someRepository.doThing();

yield someActionOrFailure.fold(
   (l) => Failure.failedForReason(l), 
   (r) => SomeGoodState.itWorked(r),
);

or

final someActionOrFailure = await _someRepository.doThing();

yield* someActionOrFailure.fold(
(l) async* {
    //do other stuff 
    yield Failure.failedForReason(l);
    }, 
(r) async* { 
 //do other stuff
 yield SomeGoodState.itWorked(r);
},
);

But I get lost trying to chain multiple calls.

Looking at some of the extension functions from ResoDev and Its1610 at issue #34 seems to get me closer, but I am not quite there.

For this event, I create an order, add an id to it, add a list of items to the order, and fetch the updated order to display. How I accomplished this before refactoring to Either, and just try catching exceptions was

        //Get an order ID
        try {
          var order = await _orderRepository.createOrderForLocation(e.locationId);

          //Update order cache
          cachedOrderId = order.id;
          cachedOrder = order;
          cachedInventoryId = order.inventoryId;

          var newOrderData = Order(inventoryId: e.inventoryId);
          //Add inventory ID to order
          await _orderRepository.patchOrderForLocation(e.locationId, cachedOrderId, newOrderData);

          //Post transactions to this order
          for (var item in e.orderItems) {
            await _orderRepository.addTransactionToOrder(e.locationId, cachedOrderId, item);
          }

          try {
            final updatedOrder = await _orderRepository.getUpdatedOrderForLocation(e.locationId, cachedOrderId);

            //Update order cache
            cachedOrderId = updatedOrder.id;
            cachedOrder = updatedOrder;
            cachedInventoryId = updatedOrder.inventoryId;

            yield CreateOrderState.orderUpdated(orderId: updatedOrder.id, order: updatedOrder);
          } catch (error) {
            yield CreateOrderState.orderError(message: 'Error: ${error.toString()}', order: cachedOrder);
          }
        } catch (error) {
          yield CreateOrderState.orderError(message: 'Error: ${error.toString()}', order: cachedOrder);
        }

Moving over to each repository function returning either success or an OrderFailure, I think this is how I should attempt this

 final newOrderData = Order(inventoryId: e.inventoryId);

        final newOrderOrFailure = await _orderRepository.createOrderForLocation(e.locationId);


        await newOrderOrFailure
             .map(
               (order) {
                 //Update order cache
                 cachedOrderId = order.id;
                 cachedOrder = order;
                 cachedInventoryId = order.inventoryId;

                 //Add inventory ID to order
                 return _orderRepository.patchOrderForLocation(e.locationId, cachedOrderId, newOrderData);
               },
             )
             .map(
               (r) => e.orderItems.map(
                 (item) => _orderRepository.addTransactionToOrder(e.locationId, cachedOrderId, item),
               ),
             )
             .map(
               (order) => _orderRepository.getUpdatedOrderForLocation(e.locationId, cachedOrderId),
             )
             .map(
                   ///Ideally, I think I should be able to yield my new state here, 
but I ran into issues as mentioned in #34, since r here is a Future<Either<Failure, Success>>
so now I await this, and then yield the fetching of an updated order
               (r) => r.nestedMap(
                 (order) {
                   //Update order cache
                   cachedOrderId = order.id;
                   cachedOrder = order;
                   cachedInventoryId = order.inventoryId;
                 },
               ),
             );

         final recommendedOrderOrFailure =
             await _orderRepository.getUpdatedOrderForLocation(e.locationId, cachedOrderId);

         yield recommendedOrderOrFailure.fold(
           (l) => CreateOrderState.orderError(orderFailure: l, order: cachedOrder),
           (order) {
             //Update order cache
             cachedOrderId = order.id;
             cachedOrder = order;
             cachedInventoryId = order.inventoryId;

             return CreateOrderState.orderUpdated(orderId: order.id, order: order);
           },
         );

And this will create an order, add an inventory id, but doesn't add any of the order items into the order. Also I feel like I can do this in a single map, but that also eludes me.

Thanks!

@spebbe
Copy link
Owner

spebbe commented Oct 30, 2020

Hi @saltedpotatos!

I am missing a lot of context here and am a bit confused by some of the code you included, such as the seemingly unnecessary nested try block in the third snippet and the purpose of yielding rather than just returning.

I also don't know the contract of OrderRepository, making the desired error handling semantics very much guesswork :-) Below, I am assuming that every method on OrderRepository returns Future<Either<SomeFailure, Order>> values that should shortcut and fail the whole computation when Left is encountered. You will have to adjust accordingly if that assumption is wrong.

I couldn't make any sense of the cachedOrder stuff, so I omitted that below. Hopefully you can add it back in a way that works for you.

But... here's an attempt based on the Future<Either> extension by @ResoDev that you linked to.
If you are interested in moving more towards an FP style, I believe that using Evaluation instead of Future<Either> directly would allow for a more elegant and natural solution.

  final newOrderData = Order(inventoryId: e.inventoryId);

  final processedOrderOrFailure = await _orderRepository.createOrderForLocation(e.locationId)

    .flatMap((newOrder) => _orderRepository.createOrderForLocation(e.locationId))

    .flatMap((recommendedOrder) => _orderRepository.patchOrderForLocation(e.locationId, recommendedOrder.id, newOrderData))

    .flatMap((patchedOrder) async {
      for(final item in e.orderItems) {
        final addResult = await _orderRepository.addTransactionToOrder(e.locationId, patchedOrder.id, item);
        if (addResult.isLeft()) {
          return addResult;
        }
      }

      return _orderRepository.getUpdatedOrderForLocation(e.locationId, patchedOrder.id);
    });

  yield processedOrderOrFailure.fold(
      (l) => CreateOrderState.orderError(orderFailure: l),
      (processedOrder) => CreateOrderState.orderUpdated(orderId: processedOrder.id, order: processedOrder));

I'm sure this doesn't do exactly what you need it to do, but I hope it at least gives you some pointers and/or ideas!

@saltedpotatos
Copy link
Author

Thanks, @spebbe , your solution worked perfectly!

Sorry about the wall of text, and still not including enough context. I try and include enough information to avoid the xy problem , but still miss the mark sometimes.

I'll have to do more reading on flatmap vs map, since I assumed I wanted to map, and apparently I needed to flatMap, and also Evaluation, since looking at the classes signature, I am not feeling enlightened.

Do you accept donations? I appreciate the help!

@spebbe
Copy link
Owner

spebbe commented Oct 31, 2020

Wow, that's great – your description clearly was pretty good then! 😃

Thank you very much for asking, but I don't accept donations.

Glad I could help – good luck with your project!

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