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.