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


Estoy tratando de validar los parámetros de un método para la 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] <- 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) )
    }
  }
Author: Sebastien Lorber, 0000-00-00

4 answers

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 Validation y algunas instancias útiles de clases de tipo sesgadas hacia la derecha para scala.Either normales. Voy a 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 nombres):

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

Estoy usando Scalaz 7 para este ejemplo. Tendrías que hacer algo menor cambios de uso 6.

Asumiré que tenemos este modelo simplificado:{[58]]}

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 se mueve a un enfoque que no implique la comprobación de 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 ValidationNel[String, A] es esencialmente lo mismo que Either[List[String], A].

Ahora usamos este método para comprobar 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 las razones discutidas aquí, por ejemplo), pero ValidationNel[String, _] es un funtor aplicativo, y estamos usando ese hecho aquí cuando "levantamos" Category.apply en él. 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 fallo con los errores acumulados: {[58]]}

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

Si todos los argumentos se hubieran comprobado, tendríamos un Success con un valor Category en su lugar.

Fallando rápido con Either

Uno de los útiles las cosas sobre el uso de funtores aplicativos para la validación es la facilidad con la que puede intercambiar su enfoque para el manejo de errores. Si desea fallar en el primero en lugar de acumularlos, esencialmente puede cambiar su método nonNull.

Necesitamos un conjunto ligeramente diferente de importaciones:

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

Pero no hay necesidad de 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 en lugar de ValidationNEL, y la instancia de funtor aplicativo predeterminada que Scalaz proporciona para Either no acumula errores.

Eso es todo lo que necesitamos hacer para obtener el comportamiento deseado de fail-fast-no son necesarios 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 algunos x: Option[Int] y obtener un Option[Int] espalda. El hecho de que Option es un funtor y por lo tanto proporciona un método map hace que esto sea fácil:

val xi = x map incremented

Hemos "levantado" incremented en el funtor Option; es decir, esencialmente hemos cambiado una función de mapeo Int a Int en una de mapeo Option[Int] a Option[Int] (aunque la sintaxis se enturbia un poco, la metáfora de "levantamiento" es mucho más clara en un lenguaje como Haskell).

Ahora supongamos que queremos aplicar el siguiente método add a x y y de una 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 es un funtor no es suficiente. El hecho de que es una mónada, sin embargo, lo es, y podemos usar flatMap para obtener lo que queremos:

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

O, equivalentemente:

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

En cierto sentido, sin embargo, la monadez de Option es excesiva para esta operación. Hay una abstracción más simple-llamado un aplicativo funtor-que es entre un funtor y una mónada y eso proporciona toda la maquinaria que necesitamos.

Tenga en cuenta que entre en un sentido formal: cada mónada es un aplicativo functor, cada aplicativo functor es un functor, pero no todo el aplicativo functor es una monada, etc.

Scalaz nos da una instancia de funtor aplicativo para Option, por lo que podemos escribir lo siguiente:

import scalaz._, std.option._, syntax.apply._

val xy = (x |@| y)(add)

La sintaxis es un poco extraña, pero el concepto no es más complicado que el funtor o ejemplos de mónadas anteriores - solo estamos elevando add en el funtor aplicativo. Si tuviéramos un método f con tres argumentos, podríamos escribir lo siguiente:{[58]]}

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

Y así sucesivamente.

Entonces, ¿por qué molestarse con los funtores 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-Validation es el ejemplo perfecto.

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

 86
Author: Travis Brown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:17

Apoyo completamente la sugerencia de Ben James de hacer un wrapper para la api que produce nulos. Pero todavía tendrás el mismo problema al escribir ese envoltorio. Así que aquí están mis sugerencias.

¿Por qué mónadas por qué para la comprensión? Un IMO de sobrecomplicación. 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, lo 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(", ")
      )
    )
  }
 7
Author: Nikita Volkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-06 22:17:27

Si te gusta el enfoque de funtor aplicativo de la respuesta de @Travis Brown, pero no te gusta la sintaxis de Scalaz o simplemente no quieres usar Scalaz, aquí hay una biblioteca simple que enriquece la biblioteca estándar de Cualquiera de las clases para actuar como una validación de funtor aplicativo: 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 eran Derechos, o lo hará devuelve una Izquierda que contiene una Lista de todos los Errores, si uno o más eran Izquierdos.

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

 4
Author: ms-tg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-27 23:47:24

Supongamos que ha completado Cualquiera de los dos 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 que devuelve:

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

Un constructor a curryfied que devuelve 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 gran cosa.

 0
Author: wiki1000,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-12 15:37:29