Shopping Basket

Creating a shopping basket application with Futures and Promises in Scala involves handling asynchronous operations, such as querying a database or making network requests, to add, remove, and fetch items in the shopping basket. Here's a simplified version of such an application.

Step 1: Setting Up the Environment

First, make sure to import necessary Scala concurrent execution contexts and other utilities:

import scala.concurrent.{Future, Promise, ExecutionContext}
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global

Step 2: Define the Models

Define a simple Item model and the ShoppingBasket class with asynchronous methods:

case class Item(id: String, name: String, price: Double, quantity: Int = 1)

class ShoppingBasket {
  private var items: Map[String, Item] = Map()

  def addItem(item: Item): Future[Unit] = Future {
    // Simulate a delay, e.g., database operation
    Thread.sleep(100)
    items.get(item.id) match {
      case Some(existingItem) =>
        items = items.updated(item.id, existingItem.copy(quantity = existingItem.quantity + item.quantity))
      case None =>
        items += (item.id -> item)
    }
  }

  def removeItem(itemId: String): Future[Unit] = Future {
    // Simulate a delay
    Thread.sleep(100)
    items -= itemId
  }

  def getTotal: Future[Double] = Future {
    // Simulate a computation delay
    Thread.sleep(100)
    items.values.map(item => item.price * item.quantity).sum
  }
}

Step 3: Implementing Asynchronous Operations

Here's how you might interact with the ShoppingBasket asynchronously, demonstrating adding items, removing an item, and calculating the total:

val basket = new ShoppingBasket()

val itemFutures = Future.sequence(Seq(
  basket.addItem(Item("1", "Apple", 0.60, 2)),
  basket.addItem(Item("2", "Banana", 0.40, 3)),
  basket.addItem(Item("3", "Carrot", 0.25, 4))
))

itemFutures.onComplete {
  case Success(_) =>
    println("Items added successfully.")
    basket.getTotal.onComplete {
      case Success(total) =>
        println(s"Total before removal: $total")
        basket.removeItem("2").onComplete {
          case Success(_) =>
            basket.getTotal.onComplete {
              case Success(newTotal) => println(s"Total after removal: $newTotal")
              case Failure(e) => println(s"Failed to get total after removal: ${e.getMessage}")
            }
          case Failure(e) => println(s"Failed to remove item: ${e.getMessage}")
        }
      case Failure(e) => println(s"Failed to get total: ${e.getMessage}")
  }
  case Failure(e) => println(s"Failed to add items: ${e.getMessage}")
}

Step 4: Error Handling and Promises

Using Promises is particularly useful when you need more control over the completion of a future, for example, in complex error handling or when integrating with callback-based APIs.

Let's say you have a method to apply a discount code, which is unpredictable and requires a Promise for better control:

def applyDiscount(code: String): Future[Double] = {
  val promise = Promise[Double]()
  
  // Simulate validating discount code asynchronously
  Future {
    Thread.sleep(100) // Simulate delay
    if (code == "DISCOUNT10") promise.success(0.1) // 10% discount
    else promise.failure(new IllegalArgumentException("Invalid discount code"))
  }
  
  promise.future.flatMap { discountRate =>
    getTotal.map(total => total * (1 - discountRate))
  }
}

In this example, applyDiscount uses a Promise to manually complete a future based on a discount code validation operation. It then uses the result to calculate the discounted total.