Method parameters validation in Scala, with for comprehension and monads
Asked Answered
H

4

35

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) )
    }
  }
Hsiuhsu answered 6/9, 2012 at 20:35 Comment(5)
It would be more idiomatic to write a wrapper around whichever library is returning possible null references. Otherwise, one can imagine duplicating this sort of validation in almost every method.Mythos
I agree with @BenJames. It's very useful to completely avoid working with nulls in Scala and use Option[..] instead where appropriate.Reticular
@BenJames my values come from Play2 json parser: JsValue. That's right, it seems i could make a more elegant solution, but i'm learning Scala and i'm still interested by the answer ;)Hsiuhsu
@PetrPudlák i totally agree, but if i work with options, this means my method signature will have to expose options... isn't it a problem since all these values are not optional?Hsiuhsu
Functions operating over simple values can be lifted to operate over Options. Look for how to do this with the Scalaz library if you are interested. Otherwise, just apply the method inside a for-comprehension.Mythos
F
88

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.

Federative answered 6/9, 2012 at 22:0 Comment(9)
Thanks. And what if i want to return only the 1st error encountered and do not execute the other statements?Hsiuhsu
Thanks. Btw what is |@| present on your code? is it a function added to Either by Scalaz?Hsiuhsu
It's "applicative builder" syntax. In short, if we have a function f that takes n arguments, (x_1 |@| x_2 |@| ... |@| x_n)(f) is that function lifted into some applicative functor. I know that's probably horribly unclear—I'll try to update the answer when I have a couple of minutes.Federative
@SebastienLorber: See appendix.Federative
@Travis Brown Is there an easy way to prevent client from using case class directly? For instance, I want to prevent the usage of: Category(myUser, myParent, myName, myDesc). Indeed, I want to force validation before object is created, thus force him to always use buildCategory method.Jill
@Mik378: In that situation I'd make Category a sealed trait with a private case class that extends it (and with buildCategory explicitly typed as ValidationNEL[Error, Category]). That way you get the convenience of a case class inside your validation object, but you have control over how people can make new instances of Category.Federative
@Travis Brown, trait and private case class extended it must be called both Category with your solution? I would rename sealed trait as CategoryBuilder, wouldn't I? Otherwise, in the signature : ValidationNEL[Error, Category], Category would refer to trait, not case class.Jill
See this example—the idea is that outsiders only ever see the trait, and the upcast in the return type is necessary to keep the private case class from leaking.Federative
What if I don't want to validate parent? In the above code I will not be able to lift Category.apply if I remove parent from validation chain. How can I get around this? ThanksTrinitrophenol
R
9

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(", ")
      )
    )
  }
Redpoll answered 6/9, 2012 at 21:49 Comment(4)
It's not unusual to want to accumulate errors when you're doing this kind of validation (so that you can e.g. tell the user all the things they got wrong), and this simplified approach doesn't really support that cleanly.Federative
@TravisBrown Makes sense. I've updated the answer to cover that.Redpoll
Actually i would like to avoid the accumulation of errors, and only return the 1st error encountered, without doing the check for other parameters.Hsiuhsu
@SebastienLorber Then simply changing collect to collectFirst will do the trick for youRedpoll
S
5

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

Schizopod answered 27/2, 2013 at 23:47 Comment(0)
V
0

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.

Villus answered 12/11, 2014 at 15:37 Comment(1)
Sorry for the horrible mutable variable explicitly set and reinited when needed, but the case is that there is a side effect to implement, and there are certainly better ways to code it.Villus

© 2022 - 2024 — McMap. All rights reserved.