I'm trying to validate the parameters of a method for nullity but i don't find the solution...
Can someone tell me how to do?
I'm trying something like this:
def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[Error,Category] = {
val errors: Option[String] = for {
_ <- Option(user).toRight("User is mandatory for a normal category").right
_ <- Option(parent).toRight("Parent category is mandatory for a normal category").right
_ <- Option(name).toRight("Name is mandatory for a normal category").right
errors : Option[String] <- Option(description).toRight("Description is mandatory for a normal category").left.toOption
} yield errors
errors match {
case Some(errorString) => Left( Error(Error.FORBIDDEN,errorString) )
case None => Right( buildTrashCategory(user) )
}
}
If you like the applicative functor approach of @Travis Brown's answer, but you don't like the Scalaz syntax or otherwise just don't want to use Scalaz, here is a simple library which enriches the standard library Either class to act as an applicative functor validation: https://github.com/youdevise/eithervalidation
For example:
In other words, this function will return a Right containing your Category if all of the Eithers were Rights, or it will return a Left containing a List of all the Errors, if one or more were Lefts.
Notice the arguably more Scala-ish and less Haskell-ish syntax, and a smaller library ;)
Lets suppose you have completed Either with the following quick and dirty stuff:
consider a validation function returning an Either:
an a curryfied constructor returning a tuple:
You can validate it with :
Not a big deal.
If you're willing to use Scalaz, it has a handful of tools that make this kind of task more convenient, including a new
Validation
class and some useful right-biased type class instances for plain oldscala.Either
. I'll give an example of each here.Accumulating errors with
Validation
First for our Scalaz imports (note that we have to hide
scalaz.Category
to avoid the name conflict):I'm using Scalaz 7 for this example. You'd need to make some minor changes to use 6.
I'll assume we have this simplified model:
Next I'll define the following validation method, which you can easily adapt if you move to an approach that doesn't involve checking for null values:
The
Nel
part stands for "non-empty list", and aValidationNel[String, A]
is essentially the same as anEither[List[String], A]
.Now we use this method to check our arguments:
Note that
Validation[Whatever, _]
isn't a monad (for reasons discussed here, for example), butValidationNel[String, _]
is an applicative functor, and we're using that fact here when we "lift"Category.apply
into it. See the appendix below for more information on applicative functors.Now if we write something like this:
We'll get a failure with the accumulated errors:
If all of the arguments had checked out, we'd have a
Success
with aCategory
value instead.Failing fast with
Either
One of the handy things about using applicative functors for validation is the ease with which you can swap out your approach to handling errors. If you want to fail on the first instead of accumulating them, you can essentially just change your
nonNull
method.We do need a slightly different set of imports:
But there's no need to change the case classes above.
Here's our new validation method:
Almost identical to the one above, except that we're using
Either
instead ofValidationNEL
, and the default applicative functor instance that Scalaz provides forEither
doesn't accumulate errors.That's all we need to do to get the desired fail-fast behavior—no changes are necessary to our
buildCategory
method. Now if we write this:The result will contain only the first error:
Exactly as we wanted.
Appendix: Quick introduction to applicative functors
Suppose we have a method with a single argument:
And suppose also that we want to apply this method to some
x: Option[Int]
and get anOption[Int]
back. The fact thatOption
is a functor and therefore provides amap
method makes this easy:We've "lifted"
incremented
into theOption
functor; that is, we've essentially changed a function mappingInt
toInt
into one mappingOption[Int]
toOption[Int]
(although the syntax muddies that up a bit—the "lifting" metaphor is much clearer in a language like Haskell).Now suppose we want to apply the following
add
method tox
andy
in a similar fashion.The fact that
Option
is a functor isn't enough. The fact that it's a monad, however, is, and we can useflatMap
to get what we want:Or, equivalently:
In a sense, though, the monadness of
Option
is overkill for this operation. There's a simpler abstraction—called an applicative functor—that's in-between a functor and a monad and that provides all the machinery we need.Note that it's in-between in a formal sense: every monad is an applicative functor, every applicative functor is a functor, but not every applicative functor is a monad, etc.
Scalaz gives us an applicative functor instance for
Option
, so we can write the following:The syntax is a little odd, but the concept isn't any more complicated than the functor or monad examples above—we're just lifting
add
into the applicative functor. If we had a methodf
with three arguments, we could write the following:And so on.
So why bother with applicative functors at all, when we've got monads? First of all, it's simply not possible to provide monad instances for some of the abstractions we want to work with—
Validation
is the perfect example.Second (and relatedly), it's just a solid development practice to use the least powerful abstraction that will get the job done. In principle this may allow optimizations that wouldn't otherwise be possible, but more importantly it makes the code we write more reusable.
I completely support Ben James' suggestion to make a wrapper for the null-producing api. But you'll still have the same problem when writing that wrapper. So here are my suggestions.
Why monads why for comprehension? An overcomplication IMO. Here's how you could do that:
Or if you insist on having the error message store the name of the parameter, you could do the following, which would require a bit more boilerplate: