Validated: control de errores

En toda aplicación software es necesario realizar tareas de validación de campos, validación de formularios,o bien, verificación de una función;y, una vez realizadas las validaciones individuales, es necesario realizar la validación del conjunto de todas ellas. En la entrada de hoy, Validated: control de errores, me centraré en describir y realizar un ejemplo de validación de los campos de un formulario representado en una estructura de tipo Map.

Supongamos que estamos desarrollando una aplicación en la cual tenemos un formulario de dos elementos: el primero, el campo nombre; y, el segundo, el campo edad. Las validaciones que tenemos que realizar son: validación del campo nombre, validación del campo edad y evaluación de la validación del conjunto del formulario.

El campo nombre debe de ser un campo que no sea vacío. El campo edad debe de ser un
campo no vacío, entero y mayor que cero.

Las validaciones se realizan mediante el tipo Validated el cual no está definido como un tipo estándar de Scala, está definido en la librería Cats. La importación del tipo Validated y el resto de importaciones necesarias para el ejemplo son las siguientes:

import cats.data.Validated
import cats.instances.list._
import cats.syntax.all._

El primer paso a realizar es realizar la definición de los alias de todas las estructuras sobre las que trabajaremos. Los alias son los siguientes:

  • Definición del formulario. El formulario es una estructura de tipo Map.
type Form = Map[String, String]
  • Definición de la estructura de control de error sencillas. Las verificaciones
    sencillas se realizarán con un elemento de tipo Either
type ControlErrorFast[A] = Either[List[String], A]
  • Definición de la estructura de control de error compleja o validación. Las verificaciones complejas se realizarán con un elemento de tipo Validated.
type ValidatedForm[A] = Validated[List[String], A]

La primera operación a realizar es la obtención de un elemento del formulario. Para
realizar dicha operación, definiremos la función getValue de la siguiente manera:

def getValue(form: Form)(campo: String) : ControlErrorFast[String] = 
  form.get(campo)
    .toRight(List(s"El valor de $campo no está especificado"))

La segunda operación es realizar la verificación del campo nombre. Para realizar dicha operación, definimos la función readName y sus funciones auxiliares. El código es el siguiente:

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
   Right(dato)
    .ensure(List(s"El campo $nombre no debe de ser vacío."))
     (_.nonEmpty)

def readName(form: Form): ControlErrorFast[String] =
  getValue(form)("name")
   .flatMap( elem => nonBlank("name")(elem))

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
  Right(dato) 
   .ensure(List(s"El campo $nombre no debe de ser vacío."))
    (_.nonEmpty)

La tercera operación es realizar la verificación del campo edad. Para realizar dicha
operación, definiremos la función readAge y sus funciones auxiliares. El código es el
siguiente:

def readAge(form: Form): ControlErrorFast[Int] =
  getValue(form)("age")
   .flatMap(nonBlank("age"))
   .flatMap(parseInt("age"))
   .flatMap(nonNegative("age"))

def parseInt(nombre:String)(age:String): ControlErrorFast[Int] =
  Either.catchOnly[NumberFormatException](age.toInt)
   .leftMap(_ => List(s"El campo $nombre debe de ser numérico"))

def nonNegative(nombre:String)(dato:Int): ControlErrorFast[Int] =
  Right(dato)
   .ensure(List(s"El campo $nombre no es válido"))
    (_ >= 0 )

Para finalizar, una vez realizadas las verificaciones de los campos, es necesario realizar la validación del formulario para obtener el listado de los posibles errores presentes en el formulario, o bien, retornar el resultado final.

La validación del formulario se realiza utilizando elementos de tipo Validated y un elemento que trabaje con los contextos de validación; dicho elemento, es un Semigroupal el cual genera una tupla de elementos del mismo contexto.

val formHtml: Form = Map("name" -> "Pepito", "age" -> "40")
val valid1_1:ValidatedForm[String] = Validated.fromEither(readName(formHtml))
val valid1_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtml))
val resultado1 = (valid1_1, valid1_2).tupled
println(s"resultado1=${resultado1}")

La salida por consola es la siguiente:

resultado1=Valid((Pepito,40))

Para el supuesto de trabajar con un formulario con datos erróneos, el código sería el siguiente:

val formHtmlKO3: Form = Map("name" -> "", "age" -> "-1")
val valid4_1:ValidatedForm[String] = Validated.fromEither(readName(formHtmlKO3))
val valid4_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtmlKO3))
val resultado4 = (valid4_1, valid4_2).tupled
println(s"resultado4=${resultado4}")
println

La salida por consola es la siguiente:

resultado4=Invalid(List(El campo name no debe de ser vacío., El campo age no es válido))

Como conclusión final, una de las opciones para el control de errores es utilizar el componente Either, pudiendo controlar los valores y las excepciones. Cuando todos los elementos Either son agrupados para su evaluación con Validated y un Semigropal podemos obtener el resultado, o bien, el conjunto de los mensajes de las no validaciones que se han producido en el mismo instante. A diferencia de otros procesos de validación, la ventaja reside en que conocemos todos los fallos producidos en el mismo tiempo.

 

Cats I: Mónada Eval

En entrada anteriores, he hablado de cierto tipo de mónada como son la mónada Reader o la mónada Estado con la librería Scalaz. En la entrada de hoy, Cats: Mónada Eval, me centraré en la mónada Eval definida en la librería Cats. La librería Cats es una de las librerías básica del ecosistema de Scala como lo es librería Scalaz.

Tipos de evaluación en Scala

La definición de variables con la palabra reservada val en Scala implica la definición de una variable inmutable. Por otro lado, si se quiere definir una variable mutable, se emplea la palabra reservada var.

Para los valores inmutables, se puede definir qué evaluación tiene una variable; es decir, se puede definir si una variables tiene evaluación inmediata o perezosa; pero, además, se puede definir si el valor es cacheado o no. Así, podemos definir variables de la siguiente forma:

  •  Variable con la palabra reservada val.– Definición de una variable con evaluación inmediata y memorizable.

Un ejemplo es el siguiente:

val x = {
  println("Procesando X")
  Math.random
}
println(x)
println(x)

La salida por consola es la siguiente:

Procesando X
0.49063463192831624
0.49063463192831624
  • Variable con las palabras reservadas lazy y val.- Definición de una variable con evaluación perezosa y memorizable.
lazy val x = {
  println("Procesando X")
  Math.random
}
println(x)
println(x)

La salida por consola es la siguiente:

Procesando X
0.7903293397160105
0.7903293397160105
  • Variable con la palabra reservada def.- Definición de una variable con evaluación perezosa y no memorizable.
def x = {
  println("Procesando X")
  Math.random
}
println(x)
println(x)

La salida por consola es la siguiente:

Procesando X
0.8181569326322171
Procesando X
0.2682764923719232

2.- Mónada Eval. Tipos de evaluación con Cats

La librería Cats define una mónada para controlar las evaluaciones. La mónada es un wrapper de un valor o de una computación que produce un valor. Además, Eval soporta una computación perezona segura para una pila mediante los métodos map y flatMap.

Los tipos de Eval son los siguientes:

  • Eval.now.- La evaluación now es equivalente a val. Un ejemplo es el siguiente:
import cats.Eval
val x = Eval.now{
println("Procesando X")
Math.random
}
println(x.value)
println(x.value)

La salida por consola es la siguiente:

Procesando X
0.8337324158188836
0.8337324158188836
  • Eval.later.- La evaluación later es equivalente a lazy val. Un ejemplo es el siguiente:
import cats.Eval
val x = Eval.later{
  println("Procesando X")
  Math.random
}
println(x.value)
println(x.value)

La salida por consola es la siguiente:

Procesando X
0.8335294714853098
0.8335294714853098
  • Eval.always.- La evaluación always es equivalente a def. Un ejemplo es el siguiente:
val x = Eval.always{
  println("Procesando X")
  Math.random
}
println(x.value)
println(x.value)

La salida por consola es la siguiente:

Procesando X
0.36159399101292466
Procesando X
0.7171589355658571
  •  Otros ejemplos:

Procesamiento de una computación Eval.always y función map

val greeting = Eval
  .always{ println("Paso 1"); "Hello"}
  .map{ str => println("Paso 2"); s"${str} world" }
println(greeting.value)

La salida por consola es la siguiente:

Paso 1
Paso 2
Hello world

Computación de operaciones Eval de tipo now y always en una for comprehension .

val ans = for{
  a <- Eval.now{ println("Calculating A"); 40 }
  b <- Eval.always{ println("Calculating B "); 2 }
}yield{
  println("Sumando A y B")
  a + b
}
println(s"Calculo1=${ans.value}")
println
println(s"Calculo2=${ans.value}")
println

La salida por consola es la siguiente:

Calculating A
Calculating B
Sumando A y B
Calculo1=42
Calculating B
Sumando A y B
Calculo2=42

3.- Otras operaciones

La mónada Eval es interesante su uso en una computación que retorne un valor; pero, hay ocasiones que se necesita cachear un valor o deferir la computación para evitar una excepción de tipo StackOverflow. Para ello, están las funciones memoize y defer.

3.1.- Función memoize.

La función memoize permite el cacheo de un valor, o bien, la cadena de una computación. Un ejemplo es el siguiente:

val saying = Eval
 .always{ println("Paso 1"); "Un gato" }
 .map{ str => println("Paso 2"); s"${str} siéntate" }
 .memoize
 .map{ str => println("Paso 3"); s"${str} la alfombra" }
println(s"Calculo1=${saying.value}")
println
println(s"Calculo2=${saying.value}")

La salida por consola es la siguiente:

Paso 1
Paso 2
Paso 3
Calculo1=Un gato siéntate la alfombra
Paso 3
Calculo2=Un gato siéntate la alfombra

3.2.- Función defer.

Las funciones para la manipulación de valores con Eval es mediante las funciones map y flatMap. Así, con las sucesivas llamadas se van apilando una conjunto de operaciones que, una vez apiladas todos los posibles casos, se realiza el cálculo.

Un ejemplo de este escenario, es el cálculo del factorial con la multiplicación del número n en cada llamada. El código es el siguiente:

def factorial(n: BigInt): Eval[BigInt] ={
  if(n==1){
    Eval.now(n)
  }else{
    factorial(n-1).map( _ * n )
  }
}
println( factorial(50000).value )

La salida por consola sería la siguiente:

Exception in thread "main" java.lang.StackOverflowError

Para evitar esta situación, podemos emplear la función defer con la cual diferimos la ejecución de una expresión que produce un valor de Eval. Es como un flatMap pero seguro.

El ejemplo anterior utilizando la función defer queda como sigue:

def factorial2(n: BigInt): Eval[BigInt] ={
  if(n==1){
    Eval.now(n)
  }else{
    Eval.defer( factorial2(n-1).map( _ * n ) )
  }
}
println( factorial2(50000).value )

La salida por consola sería el resultado del cálculo. Al ser un número muy grande, lo omito.

4.- Función fold con Eval

Para realizar el cálculo de un valor de un ADT es común la utilización de la función fold. En el siguiente ejemplo, se muestra la definición de la función fold y el cálculo de la suma de los elementos de una lista. El código es el siguiente:

def miFoldRightEvalList[A, B](list: List[A], empty: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = list match {
  case Nil => empty
  case head :: tail => Eval.defer( f(head, miFoldRightEvalList(tail, empty)(f) ) )
}

def mifoldRight[A,B](as: List[A], acc:B)(fn: (A,B) => B): B =
  miFoldRightEvalList(as, Eval.now(acc)){
    (a,b) => b.map( fn(a,_) )
  }.value

val miLista2 =  (1 to 10000000).toList
println(s"Suma de la lista= ${mifoldRight( miLista2, 0L)(_+_) } ")
println

La salida por consola es la siguiente:

Suma de la lista= 50000005000000

La mónada Eval nos permite tener una seguridad en la ejecución de una cadena de expresiones evitando el desbordamiento de la pila del sistema.