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.