🎓 A1Learner
🔍
0 of 48 lessons completed

Scala Validation Accumulation

Validation accumulation is the practice of collecting every problem in an input. It reports all of them instead of stopping at the first one.

One check returns a list with zero or one problem

An empty name gives a list with one problem. A good name gives an empty list.

def nameErrors(name: String): List[String] =
  if name.isEmpty then List("name is empty") else Nil
@main def main(): Unit =
  println(nameErrors(""))
  println(nameErrors("Ana"))

Three checks join into one list in fixed order

The three check lists join with ++. The printed list shows all three problems, and its length proves the count.

def nameErrors(name: String): List[String] =
  if name.isEmpty then List("name is empty") else Nil
def ageErrors(age: Int): List[String] =
  if age < 0 then List("age is negative") else Nil
def emailErrors(email: String): List[String] =
  if email.contains("@") then Nil else List("email needs @")
@main def main(): Unit =
  val errors = nameErrors("") ++ ageErrors(-3) ++ emailErrors("ana.example")
  println(errors)
  println(errors.length)

Either carries the full list or the good value

The good input folds to its ok message. The bad input folds to all its problems joined together.

def nameErrors(name: String): List[String] =
  if name.isEmpty then List("name is empty") else Nil
def ageErrors(age: Int): List[String] =
  if age < 0 then List("age is negative") else Nil
def validate(name: String, age: Int): Either[List[String], String] =
  val errors = nameErrors(name) ++ ageErrors(age)
  if errors.isEmpty then Right(name) else Left(errors)
@main def main(): Unit =
  println(validate("Ana", 30).fold(errs => "errors: " + errs.mkString("; "), ok => "ok: " + ok))
  println(validate("", -1).fold(errs => "errors: " + errs.mkString("; "), ok => "ok: " + ok))

Example

def nameErrors(name: String): List[String] =
  if name.isEmpty then List("name is empty") else Nil
def ageErrors(age: Int): List[String] =
  if age < 0 then List("age is negative") else Nil
def emailErrors(email: String): List[String] =
  if email.contains("@") then Nil else List("email needs @")
def validate(name: String, age: Int, email: String): Either[List[String], String] =
  val errors = nameErrors(name) ++ ageErrors(age) ++ emailErrors(email)
  if errors.isEmpty then Right("welcome " + name) else Left(errors)
@main def main(): Unit =
  println(validate("", -2, "bob.example").fold(errs => "errors: " + errs.mkString("; "), ok => ok))
  println(validate("Bob", 25, "bob@example.com").fold(errs => "errors: " + errs.mkString("; "), ok => ok))

Key points

  • Error list: problems are collected in a List, in a fixed order.
  • Fixed order: the same input always gives the same list in the same order.
  • Empty list: an empty error list means the input passed every check.
  • Either result: Either with a List on the left carries all problems or the good value on the right.
  • Combine checks: small check functions each return a list, and the lists join with ++.

In this example, three small check functions each return a list of problems. The validate function joins the three lists in a fixed order. A bad signup prints all three problems at once, and a good signup prints its welcome message.

Note

a Set of errors would print in no guaranteed order, so this course never prints one.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Scastie

Exercise

A signup has three problems. What does validation accumulation return?