पाठ 16 / 25

Futures and Asynchronous Code

Run asynchronous computations with Future and compose them.

Futures in the standard library

A Future[A] represents a value that will be available later, computed on an ExecutionContext (a thread pool). Future { ... } starts running immediately, which is called eager evaluation. Futures compose with map, flatMap, recover, recoverWith and for comprehensions, so dependent asynchronous steps read sequentially, while Future.sequence and Future.traverse run many in parallel and collect results. Callbacks such as onComplete exist, but composing is cleaner. A subtle point: in a for comprehension, futures are created when their line runs, so to run two independent futures in parallel, create them before the for. Blocking with Await.result is for tests and program edges only; in services, return the Future to the framework. Blocking I/O inside the global execution context should be wrapped in blocking { ... } or run on a dedicated pool. Because Futures are eager and memoised, they are not referentially transparent, which is one reason many teams use effect systems (next topic) instead.

Composing Futures

Independent futures run in parallel and are combined when both finish.

Two parallel lanes with progress bars starting at the same time, merging into a single result box at the end.
Figure 6.1 — Parallel futures joined by a for comprehension.

Parallel and sequential Futures

Start independent work first, then combine with for.

import scala.concurrent.{Future, Await}
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global

case class Customer(id: String, name: String)
case class Order(id: String, totalPaise: Long)

def fetchCustomer(id: String): Future[Customer] = Future { Thread.sleep(200); Customer(id, "Asha") }
def fetchOrders(id: String): Future[List[Order]] = Future { Thread.sleep(200); List(Order("o1", 120000)) }

def dashboard(id: String): Future[String] =
  val customerF = fetchCustomer(id)        // both start now, in parallel
  val ordersF = fetchOrders(id)
  for
    customer <- customerF
    orders   <- ordersF
  yield s"${customer.name}: ${orders.size} orders, total ${orders.map(_.totalPaise).sum} paise"

val safe: Future[String] = dashboard("c-1").recover { case e: Exception => s"unavailable: ${e.getMessage}" }

val all: Future[List[Customer]] = Future.traverse(List("c-1", "c-2", "c-3"))(fetchCustomer)

@main def runDashboard(): Unit =
  println(Await.result(safe, 2.seconds))   // blocking is acceptable only at the program edge
  println(Await.result(all, 2.seconds).map(_.name))

Create futures before the for

Writing for a <- fetchA(); b <- fetchB() yield ... runs the calls one after the other, because fetchB is only called after fetchA completes. Assign both futures to vals first to run them in parallel.

त्वरित जाँच: In a for comprehension over Futures, how do you make two independent calls run in parallel?

  • Use two separate for loops
  • Use Await.result inside the for
  • Start both futures (assign them to vals) before the for comprehension
  • It always runs in parallel
Answer

Start both futures (assign them to vals) before the for comprehension — Futures start when created, so creating them first lets them run concurrently.