spebbe / dartz

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

Fold Either for a list of items #69

Closed saltedpotatos closed 3 years ago

saltedpotatos commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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!