Validación de parámetros de método en Scala, con para comprensión y mónadas
monads for-comprehension (4)
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] <- 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) )
}
}
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 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 here , 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 " incremented
" incremented
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 sintaxis 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 sintaxis 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.
Si le gusta el enfoque de funcionador aplicativo de la respuesta de @Travis Brown, pero no le gusta la sintaxis 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 sintaxis 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.