Validación de parámetros de método en Scala, con para comprensión y mónadas

Estoy intentando validar los parámetros de un método de nulidad, pero no encuentro la solución …

¿Puede alguien decirme cómo hacerlo?

Estoy intentando algo como esto:

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]  Left( Error(Error.FORBIDDEN,errorString) ) case None => Right( buildTrashCategory(user) ) } } 

Si está dispuesto a usar Scalaz , tiene un puñado de herramientas que hacen que este tipo de tarea sea más conveniente, incluida una nueva clase de Validation y algunas instancias útiles de clases de tipo correctas para el viejo scala.Either . Daré un ejemplo de cada uno aquí.

Acumulando errores con Validation

Primero para nuestras importaciones de Scalaz (tenga en cuenta que tenemos que ocultar scalaz.Category para evitar el conflicto de nombre):

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

Estoy usando Scalaz 7 para este ejemplo. Tendría que hacer algunos cambios menores para usar 6.

Asumiré que tenemos este modelo simplificado:

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

A continuación, definiré el siguiente método de validación, que puede adaptar fácilmente si cambia a un enfoque que no implique buscar valores nulos:

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

La parte Nel significa “lista no vacía”, y una ValidationNel[String, A] es esencialmente la misma que una Either[List[String], A] .

Ahora usamos este método para verificar nuestros argumentos:

 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) 

Tenga en cuenta que Validation[Whatever, _] no es una mónada (por los motivos que se analizan aquí , por ejemplo), pero ValidationNel[String, _] es un funcionador aplicativo, y estamos usando ese hecho aquí cuando “levantamos” Category.apply en ella. Consulte el apéndice a continuación para obtener más información sobre los funtores aplicativos.

Ahora si escribimos algo como esto:

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

Obtendremos un error con los errores acumulados:

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

Si todos los argumentos se hubieran desprotegido, tendríamos un valor de Success con una Category lugar.

Fallando rápido con Either

Una de las cosas útiles acerca del uso de funtores aplicativos para la validación es la facilidad con la que puede cambiar su enfoque para manejar los errores. Si quiere fallar el primero en lugar de acumularlos, básicamente puede simplemente cambiar su método no nonNull .

Necesitamos un conjunto ligeramente diferente de importaciones:

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

Pero no es necesario cambiar las clases de casos anteriores.

Aquí está nuestro nuevo método de validación:

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

Casi idéntico al anterior, excepto que estamos usando Either lugar de ValidationNEL , y la instancia de functor aplicativo predeterminada que Scalaz proporciona para Either no acumula errores.

Eso es todo lo que tenemos que hacer para obtener el comportamiento deseado a prueba de fallas: no se requieren cambios en nuestro método buildCategory . Ahora si escribimos esto:

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

El resultado contendrá solo el primer error:

 Left(Parent category is mandatory for a normal category) 

Exactamente como queríamos.

Apéndice: Introducción rápida a los funtores aplicativos

Supongamos que tenemos un método con un único argumento:

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

Y supongamos también que queremos aplicar este método a alguna x: Option[Int] y obtener una Option[Int] nuevo. El hecho de que Option sea ​​un funtor y, por lo tanto, proporciona un método de map hace fácil:

 val xi = x map incremented 

Hemos ” incrementedincremented en el functor de Option ; es decir, esencialmente hemos cambiado una función mapeando Int a Int en una Option[Int] mapeo Option[Int] a Option[Int] (aunque la syntax se ensucia un poco -la metáfora de “levantar” es mucho más clara en un lenguaje como Haskell) .

Ahora supongamos que queremos aplicar el siguiente método de add a x manera similar.

 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. 

El hecho de que Option sea ​​un funtor no es suficiente. Sin embargo, es una mónada, y podemos usar flatMap para obtener lo que queremos:

 val xy: Option[Int] = x.flatMap(xv => y.map(add(xv, _))) 

O equivalente:

 val xy: Option[Int] = for { xv < - x; yv <- y } yield add(xv, yv) 

En cierto sentido, sin embargo, la mónada de Option es excesiva para esta operación. Hay una abstracción más simple, llamada functor aplicativo , que se encuentra entre un functor y una mónada y que proporciona toda la maquinaria que necesitamos.

Tenga en cuenta que está en el medio en un sentido formal: cada mónada es un funcionador aplicativo, cada funcionador aplicativo es un functor, pero no todos los funcionadores aplicativos son una mónada, etc.

Scalaz nos da una instancia de funcionador aplicativo para Option , para que podamos escribir lo siguiente:

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

La syntax es un poco extraña, pero el concepto no es más complicado que el functor o los ejemplos de mónadas anteriores: simplemente estamos add el add al functor aplicativo. Si tuviéramos un método f con tres argumentos, podríamos escribir lo siguiente:

 val xyz = (x |@| y |@| z)(f) 

Y así.

Entonces, ¿por qué molestarse con funcionadores aplicativos en absoluto, cuando tenemos mónadas? En primer lugar, simplemente no es posible proporcionar instancias de mónada para algunas de las abstracciones con las que queremos trabajar: la Validation es el ejemplo perfecto.

En segundo lugar (y relacionado), es solo una práctica de desarrollo sólida utilizar la abstracción menos poderosa que hará el trabajo. En principio, esto puede permitir optimizaciones que de otro modo no serían posibles, pero lo más importante es que hace que el código que escribimos sea más reutilizable.

Apoyo completamente la sugerencia de Ben James de crear un contenedor para la API nula. Pero igual tendrás el mismo problema al escribir esa envoltura. Así que aquí están mis sugerencias.

¿Por qué mónadas por qué para la comprensión? Una sobrecomplicación IMO. Así es como puedes hacer eso:

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

O si insiste en que el mensaje de error almacene el nombre del parámetro, podría hacer lo siguiente, que requeriría un poco más de repetición:

 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(", ") ) ) } 

Si le gusta el enfoque de funcionador aplicativo de la respuesta de @Travis Brown, pero no le gusta la syntax de Scalaz o simplemente no quiere usar Scalaz, aquí hay una biblioteca simple que enriquece la biblioteca estándar. Cualquiera de las clases para actuar como un aplicativo validación de functor: https://github.com/youdevise/eithervalidation

Por ejemplo:

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

En otras palabras, esta función devolverá un Derecho que contenga su Categoría si todos los Eithers fueron Derechos, o devolverá una Izquierda que contenga una Lista de todos los Errores, si uno o más fueron Leones.

Observe la syntax posiblemente más Scala-ish y menos Haskell-ish, y una biblioteca más pequeña;)

Supongamos que ha completado O bien con las siguientes cosas rápidas y sucias:

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

considere una función de validación devolviendo un O bien:

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

un constructor curryf devuelto una tupla:

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

Puedes validarlo con:

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

No es un gran trato.