Error Accumulation
Error accumulation is the practice of collecting every validation error instead of stopping at the first one. A user then sees all problems at once and can fix them in one pass.
One check returns an Either
A single check returns a Right value when the input passes. It returns a Left message when the input fails.
def checkAge(age: Int): Either[String, Int] =
if age >= 0 then Right(age) else Left("Age must not be negative")
@main def main(): Unit =
println(checkAge(30))
println(checkAge(-1))A fold combines many checks
The fold keeps two lists as it walks the checks. One list gathers values and the other gathers errors.
def combine(results: List[Either[String, Int]]): Either[List[String], List[Int]] =
val pair = results.foldLeft((List.empty[String], List.empty[Int])) { (acc, item) =>
item match
case Right(v) => (acc._1, acc._2 :+ v)
case Left(e) => (acc._1 :+ e, acc._2)
}
if pair._1.isEmpty then Right(pair._2) else Left(pair._1)
@main def main(): Unit =
println(combine(List(Right(3), Left("Name is empty"), Left("Age is negative"))))
println(combine(List(Right(3), Right(4))))The result shows the full picture
A match on the combined result picks one path. The good path sums the values and the bad path counts the errors.
def combine(results: List[Either[String, Int]]): Either[List[String], List[Int]] =
val pair = results.foldLeft((List.empty[String], List.empty[Int])) { (acc, item) =>
item match
case Right(v) => (acc._1, acc._2 :+ v)
case Left(e) => (acc._1 :+ e, acc._2)
}
if pair._1.isEmpty then Right(pair._2) else Left(pair._1)
@main def main(): Unit =
val good = combine(List(Right(10), Right(20)))
val bad = combine(List(Left("Name is empty"), Left("Age is negative")))
good match
case Right(values) => println(values.sum)
case Left(errors) => println(errors.length)
bad match
case Right(values) => println(values.sum)
case Left(errors) => println(errors.length)Example
def checkName(name: String): Either[String, String] =
if name.nonEmpty then Right(name) else Left("Name is empty")
def checkEmail(email: String): Either[String, String] =
if email.contains("@") then Right(email) else Left("Email needs an at sign")
def combine(results: List[Either[String, String]]): Either[List[String], List[String]] =
val pair = results.foldLeft((List.empty[String], List.empty[String])) { (acc, item) =>
item match
case Right(v) => (acc._1, acc._2 :+ v)
case Left(e) => (acc._1 :+ e, acc._2)
}
if pair._1.isEmpty then Right(pair._2) else Left(pair._1)
@main def main(): Unit =
val good = combine(List(checkName("Ada"), checkEmail("ada@example.com")))
val bad = combine(List(checkName(""), checkEmail("ada.example.com")))
good match
case Right(values) => println(values.mkString(", "))
case Left(errors) => println(errors.mkString(", "))
bad match
case Right(values) => println(values.mkString(", "))
case Left(errors) => println(errors.mkString(", "))Key points
- Validation: A validation is a check that decides whether one value is acceptable.
- Either: An Either holds a left value for failure or a right value for success.
- Error list: An Either with a list on the left side can carry every error at once.
- Fold: A fold walks a list one item at a time and builds one result from it.
- Combine step: Each check result either adds a value to the good list or adds an error to the error list.
In this example, checkName and checkEmail each return an Either for one field. The combine fold gathers the good values and the errors into separate lists. The good input prints both values and the bad input prints both errors at once.
Note
Scala code often stops at the first error with a for-comprehension, so collecting errors needs this explicit fold instead.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Scastie
Exercise
What does combine return when two of three checks fail?