Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method parameters validation in Scala, with for comprehension and monads

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) )     }   } 
like image 892
Sebastien Lorber Avatar asked Sep 06 '12 20:09

Sebastien Lorber


2 Answers

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 old scala.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):

import scalaz.{ Category => _, _ } import syntax.apply._, syntax.std.option._, syntax.validation._ 

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:

case class User(name: String) case class Category(user: User, parent: Category, name: String, desc: String) 

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:

def nonNull[A](a: A, msg: String): ValidationNel[String, A] =    Option(a).toSuccess(msg).toValidationNel 

The Nel part stands for "non-empty list", and a ValidationNel[String, A] is essentially the same as an Either[List[String], A].

Now we use this method to check 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) 

Note that Validation[Whatever, _] isn't a monad (for reasons discussed here, for example), but ValidationNel[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:

val result: ValidationNel[String, Category] =    buildCategory(User("mary"), null, null, "Some category.") 

We'll get a failure with the accumulated errors:

Failure(  NonEmptyList(    Parent category is mandatory for a normal category,    Name is mandatory for a normal category   ) ) 

If all of the arguments had checked out, we'd have a Success with a Category 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:

import scalaz.{ Category => _, _ } import syntax.apply._, std.either._ 

But there's no need to change the case classes above.

Here's our new validation method:

def nonNull[A](a: A, msg: String): Either[String, A] = Option(a).toRight(msg) 

Almost identical to the one above, except that we're using Either instead of ValidationNEL, and the default applicative functor instance that Scalaz provides for Either 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:

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) 

Exactly as we wanted.

Appendix: Quick introduction to applicative functors

Suppose we have a method with a single argument:

def incremented(i: Int): Int = i + 1 

And suppose also that we want to apply this method to some x: Option[Int] and get an Option[Int] back. The fact that Option is a functor and therefore provides a map method makes this easy:

val xi = x map incremented 

We've "lifted" incremented into the Option functor; that is, we've essentially changed a function mapping Int to Int into one mapping Option[Int] to Option[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 to x and y in a similar fashion.

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 isn't enough. The fact that it's a monad, however, is, and 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 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:

import scalaz._, std.option._, syntax.apply._  val xy = (x |@| y)(add) 

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 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 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.

like image 52
Travis Brown Avatar answered Oct 23 '22 11:10

Travis Brown


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:

def buildNormalCategory   ( user: User, parent: Category, name: String, description: String )   : Either[ Error, Category ]    = Either.cond(        !Seq(user, parent, name, description).contains(null),        buildTrashCategory(user),       Error(Error.FORBIDDEN, "null detected")     ) 

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:

def buildNormalCategory   ( user: User, parent: Category, name: String, description: String )   : Either[ Error, Category ]    = {     val nullParams       = Seq("user" -> user, "parent" -> parent,              "name" -> name, "description" -> description)           .collect{ case (n, null) => n }      Either.cond(        nullParams.isEmpty,        buildTrashCategory(user),       Error(         Error.FORBIDDEN,          "Null provided for the following parameters: " +          nullParams.mkString(", ")       )     )   } 
like image 33
Nikita Volkov Avatar answered Oct 23 '22 10:10

Nikita Volkov