If you want to use Scalaz , it has several tools that make this task more convenient, including the new Validation class and some useful instances of the direct-biased class class for the plain old scala.Either . I will give an example of each of them.
Accumulating errors with Validation
First for our Scalaz import (note that we need to hide scalaz.Category in order to avoid name conflict):
import scalaz.{ Category => _, _ } import syntax.apply._, syntax.std.option._, syntax.validation._
I am using Scalaz 7 for this example. You will need to make some minor changes to use 6.
I assume we have this simplified model:
case class User(name: String) case class Category(user: User, parent: Category, name: String, desc: String)
Next, I define the following verification method, which you can easily adapt if you go to an approach that does not include checking for null values:
def nonNull[A](a: A, msg: String): ValidationNel[String, A] = Option(a).toSuccess(msg).toValidationNel
The Nel part means a non-empty list, and the ValidationNel[String, A] is essentially the same as Either[List[String], A] .
Now we use this method to test our arguments:
def buildCategory(user: User, parent: Category, name: String, desc: String) = ( nonNull(user, "User is mandatory for a normal category") |@| nonNull(parent, "Parent category is mandatory for a normal category") |@| nonNull(name, "Name is mandatory for a normal category") |@| nonNull(desc, "Description is mandatory for a normal category") )(Category.apply)
Please note that Validation[Whatever, _] not a monad (for example, for the reasons discussed here ), but ValidationNel[String, _] is an application functor, and we will use this fact here when we βraiseβ Category.apply to it . See the appendix below for more information on applicative functors.
Now, if we write something like this:
val result: ValidationNel[String, Category] = buildCategory(User("mary"), null, null, "Some category.")
We will get a crash with accumulated errors:
Failure( NonEmptyList( Parent category is mandatory for a normal category, Name is mandatory for a normal category ) )
If all the arguments have been checked, instead, we will have Success with Category .
Failed fast with Either
One convenient way to use applicative functors to check is the ease with which you can change your approach to error handling. If you want to dump the first rather than accumulate them, you can simply change your nonNull method.
We need a slightly different set of imports:
import scalaz.{ Category => _, _ } import syntax.apply._, std.either._
But there is no need to change the case classes above.
Here is our new validation method:
def nonNull[A](a: A, msg: String): Either[String, A] = Option(a).toRight(msg)
It is almost identical to the above, except that we use Either instead of ValidationNEL , and the example of the default application functionality that Scalaz provides for Either does not accumulate errors.
This is all we need to do to get the desired fast execution - no changes are needed in our buildCategory method. Now, if we write this:
val result: Either[String, Category] = buildCategory(User("mary"), null, null, "Some category.")
The result will contain only the first error:
Left(Parent category is mandatory for a normal category)
Just the way we wanted.
Appendix: A Brief Introduction to Applicative Functors
Suppose we have a method with a single argument:
def incremented(i: Int): Int = i + 1
Suppose also that we want to apply this method to some x: Option[Int] and get Option[Int] . The fact that Option is a functor and therefore provides a map method makes this simple:
val xi = x map incremented
We "raised" incremented into the Option functor; that is, we essentially changed the mapping of the Int function to Int by one mapping of Option[Int] to Option[Int] (although the syntax mutates a bit up - the βliftingβ metaphor is much clearer in a language such as Haskell).
Now suppose we want to apply the following add method to x and y similar way.
def add(i: Int, j: Int): Int = i + j val x: Option[Int] = users.find(_.name == "John").map(_.age) val y: Option[Int] = users.find(_.name == "Mary").map(_.age) // Or whatever.
The fact that Option is a functor is not enough. The fact that this is a monad, however, we can use flatMap to get what we want:
val xy: Option[Int] = x.flatMap(xv => y.map(add(xv, _)))
Or, equivalently:
val xy: Option[Int] = for { xv <- x; yv <- y } yield add(xv, yv)
In a way, Option monad is redundant for this operation. There, a simpler abstraction called an application functor is an intermediate functor and monad, which provides all the necessary equipment.
Note that this is intermediate in the formal sense: each monad is an applied functor, each applied functor is a functor, but not every applied functor is a monad, etc.
Scalaz gives us an instance of the application functor for Option , so we can write the following:
import scalaz._, std.option._, syntax.apply._ val xy = (x |@| y)(add)
The syntax is a little strange, but the concept is no more complicated than the examples of the functor or monad above - we just raise add to the applicative functor. If we had a method f with three arguments, we could write the following:
val xyz = (x |@| y |@| z)(f)
And so on.
So why bother with applicative functors when we have monads? First, it is simply not possible to provide monad instances for some of the abstractions we want to work with - Validation is a great example.
The second (and related) is simply solid development practice to use the least powerful abstraction that does its job. In principle, this may allow optimization that would otherwise be impossible, but more importantly, it makes the code we write more reusable.