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.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s