I have the following validation method:
def validate(wine: Wine): List[Error] = {
var errors = List[Error]()
if (Validate.isEmptyWord(wine.name)) {
errors ::= ValidationError("name", "Name not specified")
} else {
if (isDuplicate(wine, "name")) {
errors ::= ValidationError("name", "There already exists a wine with the name '%s'".format(wine.name))
}
}
if (Validate.isEmptyWord(wine.grapes)) {
errors ::= ValidationError("grapes", "Grapes not specified")
}
if (Validate.isEmptyWord(wine.country)) {
errors ::= ValidationError("country", "Country not specified")
}
// more stuff like this and finnally
errors.reverse
}
You get the idea
How would you modify it to avoid the var List[Error] and make it more functional?
Scalaz provides a Validation
class that makes a functional approach to this kind of problem very easy. There's a more detailed example of how to use Validation
in this Stack Overflow answer, but I'll also give a sketch here to show how it might work in your situation. I'll assume the following setup:
case class Wine(name: String, grapes: String, country: String)
case class ValidationError(name: String, msg: String)
Now we can write a couple of validation methods (note that I'm using Scalaz 7):
import scalaz._, Scalaz._
def checkNonempty(v: String, name: String, msg: String) =
if (v.nonEmpty) v.successNel else ValidationError(name, msg).failNel
def checkDuplicate(v: String, name: String, msg: String) =
if (true) v.successNel else ValidationError(name, msg).failNel
Where of course you should add your own duplication checking to the last line. Then we can wrap it all together:
def createWine(name: String, grape: String, country: String) = (
checkNonempty(name, "name", "Name not specified").flatMap(_ =>
checkDuplicate(name, "name",
"There already exists a wine with the name '%s'".format(name)
)
) |@|
checkNonempty(grape, "grape", "Grape not specified") |@|
checkNonempty(country, "country", "Country not specified")
)(Wine.apply)
Now if we write something like this:
val result: ValidationNEL[ValidationError, Wine] = createWine(
"Whatever Estates", "Whatever Grape", "U.S."
)
We'll get a success value:
Success(Wine(Whatever Estates,Whatever Grape,U.S.))
But if we give it invalid input:
val result: ValidationNEL[ValidationError, Wine] = createWine(
"", "Some Grape", ""
)
We'll get a list of the accumulated errors:
Failure(
NonEmptyList(
ValidationError(name,Name not specified),
ValidationError(country,Country not specified)
)
)
You could also roll your own validation logic, of course, but using a library like Scalaz is probably worth the trouble if you're doing much of this kind of thing.
Write your isX
methods to return an Option[Error]
instead of a Boolean
, and then
List(
Validate.isEmptyWord(wine.name, "name", "Name not specified").
orElse( Validate.isDuplicate(wine.name, "name", "There already exists...") ),
Validate.isEmptyWord(wine.grapes, "grapes", "Grapes not specified"),
...
).flatten
and you'll have your errors. Also, there's plenty of other duplication you could remove (e.g. if it's always "x", "X not specified"
, you can just supply "x"
and have the rest filled in by isEmptyWord
).