scalaintermediate

For-Comprehensions and Monadic Composition

Use for-comprehensions with Option, Either, Future, and custom monads for elegant composition.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
import scala.util.{Try, Success, Failure}

case class User(id: Int, name: String, departmentId: Int)
case class Department(id: Int, name: String, managerId: Int)

def findUser(id: Int): Option[User] =
  Map(1 -> User(1, "Alice", 10), 2 -> User(2, "Bob", 20)).get(id)

def findDept(id: Int): Option[Department] =
  Map(10 -> Department(10, "Engineering", 1)).get(id)

def findManager(id: Int): Option[User] = findUser(id)

@main def run(): Unit =
  // Option for-comprehension
  val managerName = for
    user    <- findUser(1)
    dept    <- findDept(user.departmentId)
    manager <- findManager(dept.managerId)
  yield manager.name

  println(s"Manager: ${managerName.getOrElse("unknown")}")

  // Fails gracefully at any step
  val missing = for
    user    <- findUser(99)
    dept    <- findDept(user.departmentId)
    manager <- findManager(dept.managerId)
  yield manager.name
  println(s"Missing: $missing")  // None

  // For-comprehension with filter (guard)
  val adults = for
    age <- List(15, 22, 17, 30, 12, 25)
    if age >= 18
  yield age
  println(s"Adults: $adults")

  // Nested iteration
  val pairs = for
    x <- 1 to 3
    y <- 1 to 3
    if x != y
  yield (x, y)
  println(s"Pairs: ${pairs.toList}")

  // Either for-comprehension
  def parseInt(s: String): Either[String, Int] =
    Try(s.toInt).toEither.left.map(_ => s"'$s' is not a number")

  def validateAge(age: Int): Either[String, Int] =
    if age >= 0 && age <= 150 then Right(age)
    else Left(s"Age $age out of range")

  val validated = for
    num <- parseInt("25")
    age <- validateAge(num)
  yield s"Valid age: $age"
  println(validated)  // Right(Valid age: 25)

  val invalid = for
    num <- parseInt("abc")
    age <- validateAge(num)
  yield s"Valid age: $age"
  println(invalid)  // Left('abc' is not a number)

  // Future for-comprehension
  def fetchUser(): Future[String] = Future { Thread.sleep(50); "Alice" }
  def fetchOrders(user: String): Future[List[String]] =
    Future { Thread.sleep(50); List("Order1", "Order2") }

  val ordersFuture = for
    user   <- fetchUser()
    orders <- fetchOrders(user)
  yield s"$user has ${orders.size} orders"

  println(Await.result(ordersFuture, 5.seconds))

Use Cases

  • Chaining optional computations
  • Validation pipelines with Either
  • Async workflow composition with Future

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.