Method parameters validation in Scala, with for co

2019-01-02 20:08发布

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) )
    }
  }

4条回答
若你有天会懂
2楼-- · 2019-01-02 20:35

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:

import com.youdevise.eithervalidation.EitherValidation.Implicits._    

def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[List[Error], Category] = {     
  val validUser = Option(user).toRight(List("User is mandatory for a normal category"))
  val validParent = Option(parent).toRight(List("Parent category is mandatory for a normal category"))
  val validName = Option(name).toRight(List("Name is mandatory for a normal category"))
  Right(Category)(validUser, validParent, validName).
    left.map(_.map(errorString => Error(Error.FORBIDDEN, errorString)))
}

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 ;)

查看更多
流年柔荑漫光年
3楼-- · 2019-01-02 20:37

Lets suppose you have completed Either with the following quick and dirty stuff:

object Validation {
  var errors = List[String]()  

  implicit class Either2[X] (x: Either[String,X]){

def fmap[Y](f: X => Y) = {
  errors = List[String]()  
  //println(s"errors are $errors")
  x match {
    case Left(s) => {errors = s :: errors ; Left(errors)}
    case Right(x) => Right(f(x))
  }
}    
def fapply[Y](f: Either[List[String],X=>Y]) = {
  x match { 
    case Left(s) => {errors = s :: errors ; Left(errors)}
    case Right(v) => {
      if (f.isLeft) Left(errors) else Right(f.right.get(v))
    }
  }
}
}}

consider a validation function returning an Either:

  def whenNone (value: Option[String],msg:String): Either[String,String] = 
      if (value isEmpty) Left(msg) else Right(value.get)

an a curryfied constructor returning a tuple:

  val me = ((user:String,parent:String,name:String)=> (user,parent,name)) curried

You can validate it with :

   whenNone(None,"bad user") 
   .fapply(
   whenNone(Some("parent"), "bad parent") 
   .fapply(
   whenNone(None,"bad name") 
   .fmap(me )
   ))

Not a big deal.

查看更多
只若初见
4楼-- · 2019-01-02 20:38

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.

查看更多
残风、尘缘若梦
5楼-- · 2019-01-02 20:54

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(", ")
      )
    )
  }
查看更多
登录 后发表回答