Either and Validation
Prerequisites: Error handling: exceptions and Try
Either[L, R] holds one of two values: Left(error) or
Right(value). By convention, Right is the success ("right" as in
correct). Unlike Try, the error can be any type you choose.
enum SignupError:
case EmptyName
case InvalidEmail(email: String)
case TooYoung(age: Int)
case class Signup(name: String, email: String, age: Int)
def validateName(name: String): Either[SignupError, String] =
if name.trim.nonEmpty then Right(name.trim) else Left(SignupError.EmptyName)
def validateEmail(email: String): Either[SignupError, String] =
if email.contains("@") then Right(email) else Left(SignupError.InvalidEmail(email))
def validateAge(age: Int): Either[SignupError, Int] =
if age >= 16 then Right(age) else Left(SignupError.TooYoung(age))
def signup(name: String, email: String, age: Int): Either[SignupError, Signup] =
for
n <- validateName(name)
e <- validateEmail(email)
a <- validateAge(age)
yield Signup(n, e, a)
@main def validation(): Unit =
println(signup("Ada", "ada@example.com", 36))
println(signup("", "ada@example.com", 36))
println(signup("Sam", "sam.example.com", 12))
Right(Signup(Ada,ada@example.com,36))
Left(EmptyName)
Left(InvalidEmail(sam.example.com))
- The error type is an
enum, so callers can match on exactly what went wrong — and the compiler checks they handle every case. - The for-comprehension stops at the first
Left("fail fast"). The third signup had two problems, but only the email was reported.
Working with Either
@main def eitherOps(): Unit =
val ok: Either[String, Int] = Right(20)
val err: Either[String, Int] = Left("not a number")
println(ok.map(_ + 1))
println(err.map(_ + 1))
println(err.getOrElse(0))
println(ok.fold(e => s"error: $e", v => s"value: $v"))
println(err.left.map(_.toUpperCase))
println("7".toIntOption.toRight("not a number"))
println(Right(5).filterOrElse(_ > 10, "too small"))
Right(21)
Left(not a number)
0
value: 20
Left(NOT A NUMBER)
Right(7)
Left(too small)
Collecting all errors
To report every problem (as forms should), validate each field separately and combine the results:
def check(field: String, ok: Boolean): Either[String, Unit] =
if ok then Right(()) else Left(s"$field is invalid")
@main def allErrors(): Unit =
val checks = List(
check("name", "".nonEmpty),
check("email", "sam.example.com".contains("@")),
check("age", 12 >= 16)
)
val errors = checks.collect { case Left(e) => e }
if errors.isEmpty then println("valid") else println(errors.mkString("; "))
name is invalid; email is invalid; age is invalid
Libraries like Cats provide Validated / ValidatedNec, which
accumulate errors while combining valid results into your case class.