पाठ 11 / 25

Option, Either and Try

Handle missing values and errors as values instead of null and exceptions.

Errors as values

Idiomatic Scala avoids null and uses types to make failure visible. Option[A] is Some(value) or None for values that may be absent, such as map.get(key) or list.headOption. Either[E, A] is Left(error) or Right(value), ideal for validation and domain errors, with the error type documenting what can go wrong; Either is right-biased, so map and flatMap work on the success side. Try[A] is Success(value) or Failure(throwable), wrapping code that may throw, such as parsing or Java APIs. All three support map, flatMap, getOrElse, fold and for comprehensions, so a sequence of fallible steps stops at the first failure without nested if checks. Convert between them with toOption, toEither and toRight(error). Reserve exceptions for truly unexpected failures, and wrap Java code that may return null with Option(javaValue). Libraries such as Cats add Validated to accumulate all validation errors rather than stopping at the first.

Validating a signup with Either

Each step can fail; the for comprehension short-circuits on the first Left.

import scala.util.Try

case class Signup(name: String, email: String, age: Int)

enum SignupError:
  case MissingName
  case InvalidEmail(value: String)
  case InvalidAge(value: String)

def validName(n: String): Either[SignupError, String] =
  if n.trim.nonEmpty then Right(n.trim) else Left(SignupError.MissingName)

def validEmail(e: String): Either[SignupError, String] =
  Either.cond(e.contains("@"), e.toLowerCase, SignupError.InvalidEmail(e))

def validAge(raw: String): Either[SignupError, Int] =
  Try(raw.toInt).toOption.filter(a => a >= 13 && a <= 120).toRight(SignupError.InvalidAge(raw))

def signup(form: Map[String, String]): Either[SignupError, Signup] =
  for
    name  <- validName(form.getOrElse("name", ""))
    email <- validEmail(form.getOrElse("email", ""))
    age   <- validAge(form.getOrElse("age", ""))
  yield Signup(name, email, age)

println(signup(Map("name" -> "Asha", "email" -> "ASHA@example.com", "age" -> "29")))
// Right(Signup(Asha,asha@example.com,29))
println(signup(Map("name" -> "Ravi", "email" -> "ravi", "age" -> "x")))
// Left(InvalidEmail(ravi))

val maybeCity: Option[String] = Option(System.getenv("CITY"))   // wraps a possibly-null Java value
println(maybeCity.map(_.toUpperCase).getOrElse("UNKNOWN"))

Labelled parcels

Option, Either and Try are parcels with a label on the outside: "maybe empty", "success or a reason for failure", "success or an exception". You must look at the label before you can use the contents, so nothing explodes in your hands unexpectedly.

त्वरित जाँच: When is Either[E, A] a better choice than Option[A]?

  • When you do not care why something failed
  • When you need to report why a computation failed
  • When values can never be missing
  • When working only with Java code
Answer

When you need to report why a computation failed — Either carries an error value on the Left side, while None carries no reason.