Scalaz II: type class básicos

En la preente entrada, Scalaz II: Básico, me centraré en las types classes de Scalaz con un funcionamiento básico; como son las type classes: Equal, Show y Enum.

Scalaz Equal

La type classes Equal define funciones para realizar comparaciones. Para utilizar el API Equal, hay que importar el API y la sintaxis de Scalaz de la siguiente forma:

 import scalaz.Equal
 import scalaz.Scalaz._

Tipos básicos

Unos ejemplos de comparaciones con tipos básicos son los siguientes:

 println(s"1 === 1=>${1 === 1}")
 println(s"1 === 'algo'=>${ 1 == "algo" }")
 println(s"1.some =/= 2.some=>${1.some =/= 2.some}")
 println(s"1 assert_=== 1=>${1 assert_=== 1}")

La salida por consola del snippet anterior es el siguiente:

1 === 1=>true
1 === 'algo'=>false
1.some =/= 2.some=>true
1 assert_=== 1=>()

Ejemplos con case class

Sea una case class para definir una entidad de negocio cualquiera y unas instancias de dicha case class definidas de la siguiente forma:

 case class LuzDeTrafico(nobre: String)
 val rojo = LuzDeTrafico("rojo")
 val amarillo = LuzDeTrafico("amarillo")
 val verde = LuzDeTrafico("verde")

Sea una referencia implícita que define una operación con el API Equal con la case class anterior:

implicit val TrafficLightEqual: Equal[LuzDeTrafico] = Equal.equal(_ == _)

Se puede definir operaciones de comparación de la siguiente forma:

 println(s"rojo === rojo?${rojo === rojo}")
 println(s"rojo === amarillo?${rojo === amarillo}")

La salida por consola del snippet anterior es el siguiente:

 rojo === rojo?true
 rojo === amarillo?false

Scalaz Show

La type classes Show permite definir estructuras de datos como String. Los siguientes ejemplos muestran las funciones del API Show:

 println(s"3.show=>${3.show}")
 println(s"3.shows=>${3.shows}")
 "Saludos a la consola".println
 println(s"Saludos a la consola con show=>${ "Saludos a la consola con show".show}")

La salida por consola del snippet anterior es el siguiente:

 3.show=>3
 3.shows=>3
 "Saludos a la consola"
 Saludos a la consola con show=>"Saludos a la consola con show"

Scalaz Enum

La type class Enum permite definir estructuras secuenciales ordenadas. Las estructuras que podemos definir son las siguientes: lista de enteros, Streams de enteros y lista de caracteres. Unos snippet de ejemplo son los siguientes:

  • Definición de un rango desde los caracteres ‘a’ hasta ‘e’
 val enum1 = 'a' to 'e'
 println(s"'a' to 'e'=>${enum1}")
 println("Impresión del rango de enum1")
 enum1.foreach(println(_))
 println

La salida por la consola es la siguiente:

 'a' to 'e'=>NumericRange a to e
 Impresión del rango de enum1
 a
 b
 c
 d
 e
  • Definición de una lista con los elementos desde el caracter ‘a’ hasta ‘e’ y una lista de enteros desde 1 hasta 20.
 val enum2 = 'a' |-> 'e'
 println(s"enum2=>${enum2}")
 val enum2_1 = 1 |-> 20
 println(s"enum2_1=>${enum2_1}")

La salida por la consola es la siguiente:

 enum2=>List(a, b, c, d, e)
 enum2_1=>List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
  • Definición de un Stream de enteros con los elementos comprendidos desde el valor 3 hasta 5.
 val enum3 = 3 |=> 5
 println(s"emun3=>${enum3}")
 println(s"emun3=>${enum3.headOption}")

La salida por la consola es la siguiente:

 emun3=>scalaz.EphemeralStream$$anon$5@37e547da
 emun3=>Some(3)

Para el lector interesado, las entradas que he realizado sobre Scalaz son las siguientes:

Scalaz I: Functores y funciones como functores

Scalaz es una librería de programación funcional para el lenguaje Scala. Scalaz proporciona un conjunto de estructuras de datos para completar la biblioteca Scala; todas estas estructuras, están definidas sobre type classes y sus correspondientes instancias.

En la presente entrada, Scalaz I: Functores y funciones como functores, me centraré en los functores y en definir funciones como functores mediante los elementos de la librería Scalaz.

Definición de función curring

Las funciones curring es aquella técnica funcional nombrada en honor al matemática Haskell Curry el cual fue el creador del lenguaje funcional Haskell.

Una función curry es aquella función que define un conjunto de parámetros y, en las invocaciones a dicha función, pueden emplearse un número menor de parámetros. Así, podemos definir un conjunto de funciones a partir de una función base. Un ejemplo de función curry es la siguiente:

 def funcionBase(x:Int)(y:Int)= x + y
 val funcionCurry1 = funcionBase(2)(_)
 println(s"funcionCurry1(3)=${funcionCurry1(3)}")

La función base con técnica curring es la función funcionBase la cual define una lista de parámetros separados con paréntesis. A partir de la función base, definimos la función funcionCurry1 cuyo primer parámetro tiene el valor fijo 2 y, el segundo parámetro, un valor a determinar. Una vez definida la funcionCurry, se realiza la invocación de la función con el valor variable.

La salida por consola del ejemplo anterior es la siguiente:

 funcionCurry1(3)=5

La función funcionCurry1 tiene como primer parámetro el valor enteror 2 y, si quisiéramos, podríamos definir tantas funciones como valores enteros como primer parámetro.

Definición de Functor

La teoría de categorías es aquel estudio de las matemáticas que trata mediante axiomas estructuras abstractas como si fueran una, utilizando objetos y morfismos. Los objetos y los morfirmos forman una categoría. Así, una categoría es un conjunto de objetos y unos morfismos los cuales son las transformaciones entre dichos objetos.

Dada una categoría A, un functor es aquel morfismo que transforma la categoría A en otro categoría B. A nivel práctico, desde un punto de vista de un desarrollador, un functor se puede ver como aquella operación que transforma un elemento A en otro B y, a un bajo nivel en el lenguaje Scala, se corresponde con la función map.

Funciones como curry

En el apartado anterior, Definición de función curring, he definido una función de tipo curry y, en el presente apartado, describiré cómo se transforma una función como tipo curry. Para transformar una función como curry hay que aplicar la función curried.

En el siguiente ejemplo, se define una función curry con nombre lista1; se define una lista de enteros a la cual se aplica una función para realizar la multiplicación de los elementos por un número; y, una vez definida, se realiza la invocación de la función por el operador a multiplicar. Como se observa en la segunda línea del siguiente ejemplo, se aplica la función map con un valor entero fijo correspondiente con el segundo parámetro de la función de lista1.

 val lista1 = List(1, 2, 3, 4) map { (_: Int) * (_: Int) }.curried
 val resultadoLista1 = lista1.map {_ (9)}
 println(s"Resultado1=${resultadoLista1}")
 println

La salida por consola del ejemplo anterior es la siguiente:

 Resultado1=List(9, 18, 27, 36)

Funciones como functores

En el apartado anterior con nombre Definición de Functor, he realiza una descripción a alto nivel de un functor. En el presente apartado, describiré cómo definir functores con funciones.

Las importaciones de los elementos necesarios de Scalaz para los ejemplos siguientes, son los siguientes:

 import scala.language.higherKinds
 import scalaz.std.list._
 import scalaz.std.option._
 import scalaz.syntax.functor._
 import scalaz.{Functor, Monad}

La definición de un Functor con Scalaz de una lista de enteros y un Option de enteros es el siguiente:

 val list: List[Int] = Functor[List].map(List(1, 2, 3))(_ * 2)
 val option: Option[String] = Functor[Option].map(Some(123))(_.toString)
 println("List(1, 2, 3), [_ * 2] =>" + list)
 println("Some(123), _.toString =>" + option)

En el ejemplo anterior, defino un Functor de un tipo List y un segundo Functor de tipo Option. A estos dos functores, se aplican la función map para realizar la transformación, mediante una función, de una lista y de un Option. La función map del elemento scalaz.Functor tiene como parámetros los datos y la función que los modifica.

La salida por consola del ejemplo es la siguiente:

 List(1, 2, 3), [_ * 2] =>List(2, 4, 6)
 Some(123), _.toString =>Some(123)

En muchos casos, es necesario realizar la función de un tipo genérico sin conocer los datos; para ello, utilizamos la función lift. Así, como ejemplo, podemos definir un Functor para un Option de la siguiente manera:

 def lifted: (Option[Int]) => Option[Int] = Functor[Option].lift((x: Int) => x + 1)
 println("0.- lift((x: Int) => x + 1; Some(54) =>" + lifted(Some(54)))

La salida por consola es la siguiente:

 0.- lift((x: Int) => x + 1; Some(54) =>Some(55)

Si deseamos realizar alguna operación sobre el resultado, se puede aplicar la función map como sigue:

 println("0.1.- lift((x: Int) => x + 1; Some(54) =>" + lifted(Some(54)).map((x: Int) => x + 5))

La salida por consola es la siguiente:

 0.1.- lift((x: Int) => x + 1; Some(54) =>Some(60)

Los ejemplos descritos hasta el momento han partido de la definición de un elemento Functor; pero, si deseamos definir una función y utilizarla como un Functor o una Mónada, ¿cómo se realiza? Para ello, es necesario definir la función y emplear la función lift. Para el caso de una Mónada, un ejemplo es el siguiente:

 val func = ((x: Int) => x + 1) lift Monad[Option]
 println("1 ->" + func(Some(87)))

La salida por consola es la siguiente:

 1 ->Some(88)

Si deseamos aplicar mas transformaciones con la función map de la Mónada, se realiza de la siguiente forma:

 println("1.1 ->" + func(Some(87)).map((x: Int) => x + 1))
 println("1.2 ->" + func(Some(87)).map((x: Int) => x + 1).map((x: Int) => x + 10))

La salida por consola es la siguiente:

 1.1 ->Some(89)
 1.2 ->Some(99)

Si deseamos definir la transformación como una función, se puede realizar de la siguiente forma:

 def func2(elem: Int) = func(Some(elem)).map((x: Int) => x + 1).map((x: Int) => x + 10)
 println("1.3 ->" + func2(87))

La salida por consola es la siguiente:

 1.3 ->Some(99)

La definición de una función para realizar un morfismo de una lista de enteros en otra lista de enteros, se puede definir de la siguiente manera.

 def liftedListInt: (List[Int] => List[Int]) = Functor[List].lift((x: Int) => x * 5)
 println("2 ->" + liftedListInt(List(1, 2, 3)))
 println("2.1 ->" + liftedListInt(List(1, 2, 3)).map((x: Int) => x * 5))

La salida por consola es la siguiente:

 2 ->List(5, 10, 15)
 2.1 ->List(25, 50, 75)

Otras funciones

Scalaz es un conjunto de componentes de type classes y, el patrón type classes, permite la definición de sintaxis. En el presente apartado, se identifican un conjunto de funciones pertenecientes a la sintaxis de functores. La importación de los elementos necesarios para los ejemplos son las siguientes:

 import scalaz.Functor
 import scalaz.Scalaz._

La selección de las funciones que he realizado son las siguientes:

  • Para convertir todos los elementos de una estructura a un elemento determinado, se emplea la función >|. Un ejemplo es el siguiente:
 val listaX = List(1,2,3) >| "x"
 println(s"List(1,2,3) >| 'x'=${listaX}")

La salida por consola es la siguiente:

 List(1,2,3) >| 'x'=List(x, x, x)
  • Para convertir todos los elementos de una estructura a un elemento determinado, se emplea la función as. Un ejemplo es el siguiente:
 val listaAs = List(1,2,3) as "x"
 println(s"List(1,2,3) as 'x'=${listaAs}")

La salida por consola es la siguiente:

 List(1,2,3) as 'x'=List(x, x, x)
  • Para convertir un conjunto de entrada en un conjunto de tuplas con elementos duplicados, se emplea la función fpair. Un ejemplo es el siguiente:
 val listaFPair = List(1,2,3).fpair
 println(s"List(1,2,3).fpair=${listaFPair}")

La salida por consola es la siguiente:

 List(1,2,3).fpair=List((1,1), (2,2), (3,3))
  • Para convertir un conjunto de entrada en un conjunto de duplas cuyo elemento derecho sea uno determinada, se emplea la función strengthR. Un ejemplo es el siguiente:
 val listaStrengthR = List(1,2,3).strengthR("x")
 println(s"List(1,2,3).strengthR('x')=${listaStrengthR}")

La salida por consola es la siguiente:

 List(1,2,3).strengthR('x')=List((1,x), (2,x), (3,x))
  • Para convertir un conjunto de entrada en un conjunto de duplas cuyo elemento izquierdo sea uno determinada, se emplea la función strengthL. Un ejemplo es el siguiente:
 val listaStrengthL = List(1,2,3).strengthL("x")
 println(s"List(1,2,3).strengthL('x')=${listaStrengthL}")

La salida por consola es la siguiente:

 List(1,2,3).strengthL('x')=List((x,1), (x,2), (x,3))
  • Para convertir un conjunto de entrada en un conjunto de tuplas vacías, se emplea la función void. Un ejemplo es el siguiente:
 val listaVoid = List(1,2,3).void
 println(s"List(1,2,3).void=${listaVoid}")

La salida por consola es la siguiente:

 List(1,2,3).void=List((), (), ())

La definición de un Functor o una Mónada mediante el patrón funcional type classes es una operación que requiere, para el desarrollador principiante en la programación funcional, un entendimiento de conceptos complejos. Mediante la utilización de Scalaz utilizar Functores y definir funciones como functores es un proceso comprensible y fácil de entender.

Para el lector interesado, las entradas que he realizado sobre Scalaz son las siguientes:

Notas de programación funcional

En la entrada de hoy, «Notas de programación funcional», describiré ciertos conceptos generales de la programación funcional. Serán pequeñas píldoras documentales, o bien, mis notas sobre los conceptos iniciales en la programación funcional. No pretendo realizar definiciones formales, ni que la entrada sea formal; solo pretendo crear pequeñas notas las cuáles sirvan para allanar los primeros pasos en el estudio de la programación funcional.

El lenguaje Scala es un lenguaje híbrido con en el que se aplican dos paradigmas: el paradigma funcional y el paradigma orientado a objetos. El creador del lenguaje es Martin Odersky en el 2004. El lenguaje Scala está basado en el lenguaje Haskell y el lenguaje Erlang, adquiriendo de estos dos lenguajes, lo mejor de ellos. Del lenguaje Haskell, los principios de la programación funcional y, del lenguaje Erlang, el modelo de actores para la programación concurrente.

Funciones

En Scala hay diferentes tipos de funciones: funciones totales, funciones parciales, first class function,…No voy a realizar una descripción total de todas ellas, pero me centraré en el concepto de función genérico.

Definimos una función con la siguiente estructura:

 def nombreFunción([lista de parámetros]): [Tipo de retorno] = {
   Cuerpo de la función
   return [expresiónRetorno]
 }

Un ejemplo de función es la siguiente:

 def suma( operador1:Int, operador2:Int ): Int = {
   var suma: Int = 0
   suma = operador1 + operador2
   return suma
 }

En Scala, tenemos la posibilidad de definir funciones como variables, sin la necesidad de definir una función de forma específica; este caso, se conoce como first class function. Un ejemplo de first class function es el siguiente:

 val suma: (Int,Int) => Int = (a:Int, b:Int) => a + b
 println(s"suma(2,3)=${suma(2,3)}")
 println

El compilador Scala, realiza la transformación de la función suma como una clase de tipo Function, permitiendo la posibilidad de definir este tipo de variables función de forma sencilla.

Las funciones parciales son aquellas funciones en donde se define parte de la función. En el siguiente ejemplo, defino una función parcial suma en donde se define el primer operando y, el segundo operando, es pasado por parámetro.

val suma2: PartialFunction[Int, Int] = {
 case d:Int => 2 + d
 }
 println(s"suma2(2+3)=${suma2(3)}")
 println

Recursividad

La recursividad es aquella forma en la cual una función se define basada en su propia definición. Una función recursiva se define en función de dos pasos: el caso base, solución a la instancia mas sencilla; y, el caso de inducción, solución a los casos complejos.

Un ejemplo típico de recursividad es la función factorial, la cual se define en lenguaje Scala como sigue:

 def factorial(n: Int): Int = {
   if (n > 1)
     n * factorial(n-1)
   else
     1
   }
 println(s"factorial(3)=${factorial(3)}")
 println

Las funciones recursivas son utilizas en estructuras de datos recursivas, como por ejemplo, una lista o un árbol.

Existe dos tipos de recursividad: la recursividad por la cabecera y la recursividad por la cola.

La recursividad por la cabecera es aquella recursividad que se realiza aplicando el cálculo de la operación con la cabecera de la estructura y, una vez realizada, se realiza la llamada recursiva con la cola. Con esta recursividad, para el caso de una lista, se realiza el recorrido desde el inicio de la estructura (cabecera) hasta el final de la lista.

La recursividad por la cola es aquella recursividad que se realiza aplicando las llamadas recursivas a la función y, una vez llamadas, se realiza el cálculo; es decir, recorres la lista con las llamadas y, en el retorno de la recursividad, aplicas el cálculo. Con esta recursividad, para el caso de una lista, se realiza el recorrido desde el final de la estructura hasta el inicio.

ADT

Scala es un lenguaje tipado, como por ejemplo: tipo entero, Int; tipo alfanumérico, String; tipo lógico, Boolean;… Con la combinación de estos tipos, podemos definir tipos más complejos, mediante las operaciones suma y producto.

Definimos un ADT (Algebraic Data Type), tipo de datos algebraico, como aquel tipo conformado por tipos simples los cuales, mediante las operaciones de suma y producto, formamos tipos más complejos. La operación suma es aquella operación que corresponde con la herencia y, la operación producto, es aquella operación que corresponde con las definiciones de los parámetros de las case class.

Un ejemplo de un ADT que define un tipo Lista con tipos enteros, es el siguiente:

sealed trait MiLista
case class Null extends MiLista
case class Nodo(cabeza:Int, cola:MiLista)

De la definición anterior, podemos decir que tenemos la definición de una estructura de tipo lista de enteros de una forma matemática clara y sencilla; pero, ¿y si queremos definir una lista de otro tipo? La primera solución, es definir otro ADT para el tipo seleccionado; y, la segunda solución, consiste en definir un ADT con la capacidad de soportar polimosfirmo paramétrico. Un ejemplo de una lista con polimorfísmo paramétrico es el siguiente:

sealed trait MiLista[T]
case class Null[T]() extends MiLista[T]
case class Nodo[T](cabeza:[T], cola:MiLista[T])

Con la definición anterior, podemos definir una lista de cualquier tipo.

Funciones de Orden Superior (HOF – Hight Order Function)

Las funciones de orden superior son aquellas funciones que permiten definir como parámetro otra función, es decir, un parámetro de la función puede ser una función. Un ejemplo básico y sencillo de función HOF, puede ser el siguiente:

 def ejemploHOF(mensaje:String, f: (Int,Int) => Int, operador1:Int, operador2:Int ): Unit = {
   println(mensaje + "=" + f(operador1, operador2))
 }
 ejemploHOF( "Resultado de la suma(2,3)", suma, 2, 3 )

La salida por consola del ejemplo es el siguiente:

Resultado de la suma(2,3)=5

El ejemplo parece sencillo pero a partir de este concepto se construyen muchos patrones de la programación funcional que no son tan sencillos.

Catamorfismo

En los apartados anteriores describí conceptos como los ADT’s y funciones de orden superior; pero, llegado a este punto, hay que preguntarse: ¿cómo trabajo con los ADT’s?, ¿cómo realizo un cálculo sobre un ADT?, ¿con qué mecanismos los puedo manipular? La respuesta es sencilla, trabajamos los ADT’s con los catamorfismos.

Definimos catamorfismo como aquella formar de interpretar, manipular o consumir un ADT. Se puede definir con cualquier ADT y, a nivel práctico, se corresponde con la función fold.

El ADT es aquel tipo definido desde un punto de vista matemático y, la programación funcional, tiene un aspecto matemático; con lo cual, la forma de pensar debe de ser matemática.

Sea el ADT que define la estructura Lista del apartado anterior definido de la siguiente forma:

sealed trait MiLista[T] // Caso abstracto
case class Null[T]() extends MiLista[T] // Caso Base
case class Nodo[T](cabeza:[T], cola:MiLista[T]) extends MiLista[T]// Caso de Inducción

A nivel conceptual, una lista se puede representar con la anterior definición de la siguiente forma:

Lista(1,2,3) => val lista: MiLista[Int] = Nodo(1, Nodo(2, Nodo(3, Null) ) )

Dado el siguiente problema a resolver: cálculo de la suma de los elementos de una lista, es decir: dada un elemento de tipo MiLista[Int], se desea definir una función que calcule la suma de los elementos y retorne un número entero.

La definición del ADT está formada por tres elementos: caso base, caso de inducción y caso abstracto. Con estos tres casos definimos el catamorfismo de la siguiente forma:

  • Caso Base. Dado un elemento de tipo Null[Int], ¿qué tengo que realizar para retornar como resultado un entero(B)?.
Null[Int] ---(B)---> Int; Resultado: B=0

Dado el caso base, tenemos que retornar el valor 0. Para toda lista vacía, la suma de los elementos de una lista es cero.

  •  Caso de inducción. Dado un elemento de tipo Nodo[Int], ¿qué tengo que realizar para calcular la suma de sus elementos(B)?
Node[Int] -----(A:Int, B:MiLista[Int]) => B(Int)---> Int; Resultado: (A, B) => B

Un Node está formado por una cabeza(parte A) y un cola de tipo MiLista(parte B). La suma de la parte A y de la parte B tiene que tener como resultado un número B entero, es decir, todo elemento A mas B (resultado de la cola) tiene que tener como resultado B

  • Caso abstracto. El caso abstracto lo forma la unión del caso base y el caso de inducción; es decir, la operación suma y la operación producto. Así, tenemos:
    • caso base: Null[Int] => B
    • caso inducción: Nodo[T](cabeza:[T], cola:MiLista[T]); (A, B) => B
    • caso abstracto: caso base más caso inducción: Null[Int] U Nodo[T](cabeza:[T], cola:MiLista[T]) => (B, (A,B)=> B)

Así, la función fold queda definida de la siguiente forma:

 def fold[A,B](lista:MiLista[A])(base:B, f: (A, B) => B): B = lista match{
   case Null() => base
   case Nodo(h,t) => f(h, fold(t)(base,f))
 }

Fold, FoldRight

La función fold es idéntica a la función foldRight. Estas funciones están enfocadas a una recursividad de cabacera.

Así, la función foldRight queda definida como sigue:

 def foldRight[A, B](lista: MiLista[A])(zero: B,f: (A, B) => B): B = lista match {
   case Nil => zero
   case head::tail => f(head, foldRight(tail)(zero,f))
 }

Desde un punto de vista de las llamadas a realizar y qué elementos intervienen en cada una de las llamadas, se puede ver como sigue:

-> 1 + foldRight(–)
-> 1 + (2 + foldRight(–) )
-> 1 + (2 + (3 + foldRight(–) ) )
-> 1 + (2 + (3 + ( 4 + foldRight(–) ) ) )

Por cada llamada, se van insertando una entrada en la pila del sistema. Cuando se termina el espacio, la ejecución falla. Un ejemplo para llegar a esta situación es el siguiente:

 val listaEnteros = (1 to 325000).toList
 println( foldRight(listaEnteros)(0, (a:Int, b:Int) => a + b) )

La salida por consola es la siguiente:

Exception in thread "main" java.lang.StackOverflowError

FoldLeft

La función foldLeft tiene un parecido con las funciones fold y foldRight, la diferencia, reside en el orden de los parámetros de la función pasada por parámetro. El tipo de recursividad es por la cola, es decir, se realiza la llamada recursiva y, una vez llamada, se realiza el cálculo. La función foldLeft se define como sigue:

@annotation.tailrec
 def foldLeft[A,B](l:List[A])(zero:B, f:(B,A)=>B): B = l match {
 case Nil => zero
 case head::tail => foldLeft(tail)( f(zero,head), f )
 }

Este tipo de recursión en Scala es la más eficiente porque se llama a la función con el valor acumulado.

Para demostrar la eficiencia de esta función, realizamos el cálculo de la suma de la lista de ejemplo de foldRight.

 val listaEnteros = (1 to 325000).toList
 println( foldLeft(listaEnteros)(0, (a:Int, b:Int) => a + b) )

La salida por consola es la siguiente:

1273054948

Como he comentado al inicio de la entrada, no he querido ser formal, simplemente he querido mostrar pequeñas píldoras documentales para que, a la persona iniciada en la programación funcional, le permita comprender las piedras iniciales. Desde mi experiencia, pasadas estas primeras piedras, el proceso de comprensión del resto de conceptos es más sencillo.

Circe IV: ópticas

Finalizo la serie de Circe con la presente entrada, Circe IV: ópticas, en la cual me centraré en cómo Circe emplea componentes ópticos para facilitar su funcionalidad. Circe no implementa ópticas, sino que se ayuda de la librería Monocle para implementar esta funcionalidad.

 

El sketchnote de la presente entrada, queda descrito en la siguiente imagen:

Para el lector interesado en la librería óptica Monocle, puede acceder a los siguientes enlaces de la librería Monocle que tengo publicados:

El primer paso a realizar es definir una estructura JSON de prueba con lo cual realizar la comparativa entre el modo de trabajo sin ópticas y con ópticas. El JSON de pruebas es el siguiente:

 import cats.syntax.either._
 import io.circe._
 import io.circe.parser._
 val json: Json = parse(
 """
 {
   "order": {
   "customer": {
   "name": "Custy McCustomer",
   "contactDetails": {
     "address": "1 Fake Street, London, England",
     "phone": "0123-456-789"
   }
 },
 "items": [{
   "id": 123,
   "description": "banana",
   "quantity": 10
  }, {
      "id": 456,
      "description": "apple",
      "quantity": 20
    }],
    "total": 123.45
  }
 }
 """).getOrElse(Json.Null)

Acceso a datos en Circe sin ópticas

Como he descrito en las entradas anteriores al tema, el acceso a los datos se realiza con un cursor, la función downField y get entre otros. Para refrescar los conceptos, en el siguiente snippet se definen dos ejemplos: el primero, acceso al campo Phone del JSON de prueba; y, el segundo, acceso a los valores de un array del JSON de prueba. El código es el siguiente:

 println(s"[*] Número de teléfono del cliente(NO ÓPTICA): 
     ${json.hcursor.downField("order")
        .downField("customer")
        .downField("contactDetails")
        .get[String]("phone").toOption}")
 val items: Vector[Json] = json.hcursor.downField("order").downField("items").
 focus.flatMap(_.asArray).
 getOrElse(Vector.empty)
 val quantities: Vector[Int] = items.flatMap( _.hcursor.get[Int]("quantity").toOption )
 println(s"[*] Obtención de un Array del JSON=${quantities} ")

La salida por consola es la siguiente:

 [*] Número de teléfono del cliente(NO ÓPTICA): Some(0123-456-789)
 [*] Obtención de un Array del JSON=Vector(10, 20)

Acceso a datos en Circe con ópticas

La utilización de ópticas supone la definición de un regla de acceso para cada campo. Así, tenemos que definir para cada campo una óptica. Para los ejemplos del apartado anterior, definimos las ópticas para el campo phone y el array quantity de la siguiente forma:

 import io.circe.optics.JsonPath._
 val _phoneNum = root.order.customer.contactDetails.phone.string
 println(s"Número de teléfono del cliente (ÓPTICA): ${_phoneNum.getOption(json)}")
 val items: List[Int] = root.order.items.each.quantity.int.getAll(json)
 println(s"Número de teléfono del cliente (ÓPTICA): ${items}")

La salida por consola es la siguiente:

 Número de teléfono del cliente (ÓPTICA): Some(0123-456-789)
 Número de teléfono del cliente (ÓPTICA): List(10, 20)

Modificación de datos en Circe con ópticas

Para realizar la moficación de un campo, se emplea la función modify de la óptica de aquel campo a modificar. Para realizar la modificación del campo quantity, se realiza de la siguiente forma:

 import io.circe.optics.JsonPath._
 import io.circe._
 val doubleQuantities: Json => Json = root.order.items.each.quantity.int.modify(_ * 2)
 println(s"Multiplicación del campo quantity x2= ${doubleQuantities(json)} ")

La salida por consola es la siguiente:

 Multiplicación del campo quantity x2= {
   "order" : {
   "customer" : {
   "name" : "Custy McCustomer",
   "contactDetails" : {
     "address" : "1 Fake Street, London, England",
     "phone" : "0123-456-789"
   }
 },
  "items" : [
   {
     "id" : 123,
     "description" : "banana",
     "quantity" : 20
   },
   {
     "id" : 456,
     "description" : "apple",
     "quantity" : 40
   }
  ],
   "total" : 123.45
  }
 }

Llegado a este punto, podemos llegar a la conclusión final que la operativa con estructuras JSON con la librería Circe es una tarea sencilla y de fácil aprendizaje: el acceso, modificación y transformación de JSON a una case class o viceversa, son tareas con una pequeña complejidad. Además, si se conoce el funcionamiento de las librerías ópticas, el acceso y manipulación de JSON es más sencillo aún.

Para el lector interesado, el conjunto de las entradas de la librería Circe son las siguientes:

Circe III: codificacores y decodificadores

Continúo con la serie de entradas de la serie sobre la librería Circe. En la presente entrada, Circe III: codificacores y decodificadores, me centraré en los codificadores y decodificadores de Circe los cuáles se definen como type classes.

El sketchnote de la presente entrada, queda descrito en la siguiente imagen:

Un codificador, Encoder[A], contiene una función que convierte un elemento de tipo A en un JSON; y, un decodificador, Decoder[A], realiza la función inversa de un JSON de un tipo A. La librería Circe contiene instancias implícitas de estas type classes para muchos tipos de scala como Int, String, List[A], Option[A] y otros.

Operaciones básicas

Para realizar las operaciones básicas es necesario importar los elementos de los siguientes paquetes:

 import io.circe.parser.decode
 import io.circe.syntax._
  • Para codificar una lista en JSON podemos utilizar la función asJson, o bien, as[TipoDato]. Unos ejemplos ilustrativos son los siguientes:
 val intsJson = List(1, 2, 3).asJson
 println(s"[-] Codificación usando la sintaxis: List->JSON=${List(1, 2, 3).asJson}")
 println(s"[-] Decodificación usando la sintaxis de Json->List[Int]=${intsJson.as[List[Int]]}")

La salida por consola es la siguiente:

 [-] Codificación usando la sintaxis: List->JSON=[
   1,
   2,
   3
 ]
 [-] Decodificación usando la sintaxis de Json->List[Int]=Right(List(1, 2, 3))
  • Para decodificar una estructura JSON a un tipo determinado, se emplea la función decode. Unos ejemplos ilustrativos son los siguientes:
 println(s"[-] Decodificación String->List[Int]=${decode[List[Int]]("[1, 2, 3]")}")
 println(s"[-] Decodificación String->Seq[Int]=${decode[Seq[Int]]("[1, 2, 3]")}")
 println(s"[-] Decodificación String->List[Option[Int]]=${decode[List[Option[Int]]]("[1, 2, 3]")}")

La salida por consola es la siguiente:

 [-] Decodificación String->List[Int]=Right(List(1, 2, 3))
 [-] Decodificación String->Seq[Int]=Right(List(1, 2, 3))
 [-] Decodificación String->List[Option[Int]]=Right(List(Some(1), Some(2), Some(3)))

Operaciones semiautomáticas

En ciertos momentos es necesario tener definidos Encoder y Decoder para una case class determinada. Para ello, utilizamos los componentes semiautomáticos y los importamos de la siguiente manera:

 import io.circe._
 import io.circe.generic.semiauto._

Definimos una case class con nombre Foo de prueba como sigue:

 case class Foo(a: Int, b: String, c: Boolean)

Definimos los codificadores y decodificadores para la case class de forma implícita de la siguiente forma:

 implicit val fooDecoder: Decoder[Foo] = deriveDecoder[Foo]
 implicit val fooEncoder: Encoder[Foo] = deriveEncoder[Foo]

La operación de codificar la case class Foo a un JSON y su proceso contrario es el siguiente:

 println(s"[*] Encoder Foo->Json =${fooEncoder(Foo(a = 1, b = "b", c = true))}")
 val jsonFoo = fooEncoder(Foo(a = 2, b = "bbb", c = false))
 val cursor: HCursor = jsonFoo.hcursor
 println(s"[*] Decoder Json->Foo =${fooDecoder(cursor)}")

La salida por consola es la siguiente:

 [*] Encoder Foo->Json ={
   "a" : 1,
   "b" : "b",
   "c" : true
 }
 [*] Decoder Json->Foo =Right(Foo(2,bbb,false))

Operaciones con anotaciones

La librería Circe contiene unas anotaciones que permite realizar operaciones de codificación, permitiendo la codificación de un case class a JSON. La anotación se encuentra definida en el siguiente paquete io.circe.generic.JsonCodec. Así, un ejemplo de codificación de case class a JSON es el siguiente:

 import io.circe.syntax._
 import io.circe.generic.JsonCodec
 @JsonCodec case class Bar(i: Int, s: String)
 println(s"[-] Conversión Bar->JSON=${Bar(i = 1, s = "Prueba").asJson}")

La salida por consola es la siguiente:

 [-] Conversión Bar->JSON={
   "i" : 1,
   "s" : "Prueba"
 }
  • Para una relación de clases anidadas, la utilización de anotaciones permite una automatización sencilla y eficiente. Así, dadas las siguientes entidades:
 import io.circe.generic.JsonCodec
 @JsonCodec case class Person2(name: String)
 @JsonCodec case class Greeting2(salutation: String, person: Person2, exclamationMarks: Int)
  • La operación de conversión de una entidad a JSON, se realiza de la siguiente forma:
 println(s"[*] CLASE COMPLEJA -> JSON, usando @JsonCodec=${Greeting2("Hey", Person2("Chris"), 3).asJson}")

La salida por consola es la siguiente:

 [*] CLASE COMPLEJA -> JSON, usando @JsonCodec={
   "salutation" : "Hey",
   "person" : {
   "name" : "Chris"
  },
   "exclamationMarks" : 3
 }
  • La operación de conversión de JSON a la entidad, se realiza de la siguiente forma:
 val greetingJSON =
 """
  {
    "salutation" : "Hey",
    "person" : {
    "name" : "Chris"
  },
    "exclamationMarks" : 3
  }
 """
 import io.circe.parser.decode
 println(s"Decodificacion=${decode[Greeting2](greetingJSON)}")

La salida por consola es la siguiente:

Decodificacion=Right(Greeting2(Hey,Person2(Chris),3))

Helper method

En ciertos momentos es necesario tener un codificador y un decodificador para una determinada entidad de negocio. Para ello, definimos un objeto con métodos helper de forma implícita. Así, para la siguiente entidad User definimos la clase codificadora de la siguiente forma:

  import io.circe.{Decoder, Encoder}
  case class User(id: Long, firstName: String, lastName: String)
  object UserCodec {
    implicit val decodeUser: Decoder[User] =
      Decoder.forProduct3("id", "first_name", "last_name")(User.apply)
    implicit val encodeUser: Encoder[User] =
      Encoder.forProduct3("id", "first_name", "last_name")(u =>
        (u.id, u.firstName, u.lastName)
      )
  }

En este caso hemos usado la función forProduct3 porque la entidad User tiene tres campos; pero, existen funciones forProductN (siendo N un número natural salvo el cero) para las clases con distinto número de campos. Un ejemplo de utilización de los helper definidos en el objeto UserCodec es el siguiente:

  println(s"[--] Codificación User->JSON =${encodeUser(User(id = 1, firstName = "11", lastName = "111"))}")
  val jsonPrueba = encodeUser(User(id = 69, firstName = "Prueba Decodificación", lastName = "Prueba Decodificación"))
  val cursor: HCursor = jsonPrueba.hcursor
  println(s"[--] Decodificación JSON->User =${decodeUser(cursor)}")

La salida por consola es la siguiente:

 [--] Codificación User->JSON ={
   "id" : 1,
   "first_name" : "11",
   "last_name" : "111"
  }
 [--] Decodificación JSON->User =Right(User(69,Prueba Decodificación,Prueba Decodificación))

Codificadores y Decodificadores a medida

En Circe existe la posibilidad de definir codificadores y decodificadores a medida. Para esta operación, utilizamos las entidades Encoder y Decoder. Así, para una entidad ejemplo Thing, las clases codificadores y decodificadoras se definen de la siguiente manera:

 import io.circe.{ Decoder, Encoder, HCursor, Json }
 class Thing(val foo: String, val bar: Int)
 implicit val encodeFoo: Encoder[Thing] = new Encoder[Thing] {
   final def apply(a: Thing): Json = Json.obj(
     ("foo", Json.fromString(a.foo)),
     ("bar", Json.fromInt(a.bar))
   )
 }
 implicit val decodeFoo2: Decoder[Thing] = new Decoder[Thing] {
   final def apply(c: HCursor): Decoder.Result[Thing] =
   for {
     foo <- c.downField("foo").as[String].right
     bar <- c.downField("bar").as[Int].right
   } yield {
     new Thing(foo, bar)
   }
  }

Para realizar la operación de codificación de un objeto de la clase Thing a un JSON, se realiza de la siguiente manera:

 println(s"[A*] Codificador de una clase THING->JSON = ${encodeFoo(new Thing("PruebaCliente",12))} ")

La salida por consola es la siguiente:

 [A*] Codificador de una clase THING->JSON = {
   "foo" : "PruebaCliente",
   "bar" : 12
 }

Para acceder a los valores de los campos definidos en el JSON, se realiza empleando el campo downField. La obtención de los campos del JSON anterior, se realiza de la siguiente forma:

 val cursorPruebaCodificada: HCursor = pruebaCodificado.hcursor
 println(s"[A*] Campo bar = ${cursorPruebaCodificada.downField("bar").as[Int].right } ")
 println(s"[A*] Campo foo = ${cursorPruebaCodificada.downField("foo").as[String].right } ")
 println(s"[A*] Campo JSON = ${cursorPruebaCodificada.top } ")

La salida por consola es la siguiente:

 [A*] Campo bar = RightProjection(Right(12)) 
 [A*] Campo foo = RightProjection(Right(PruebaCliente)) 
 [A*] Campo JSON = Some({
   "foo" : "PruebaCliente",
   "bar" : 12
 })

Para realizar la operación de decodificar un JSON a un objeto Thing, se utiliza un Decoder de la siguiente forma:

println(s"[A*] Decodificador JSON -> Thing = ${decodeFoo2( cursorPruebaCodificada ).right.e.right.get } ")

La salida por consola es la siguiente:

[A*] Decodificador JSON -> Thing = Thing(PruebaCliente,12)

Codificadores de claves

La existencia y sencillez de uso de los codificadores y decodificadores, permite emplear su funcionalidad para otras tareas como las codificaciones de claves. Circe proporciona dos elementos para para la codificación y decodificación de claves, respectivamente KeyEncoder y KeyDecoder.

Sea una clave definida por una clase Foo como sigue:

case class Foo(value: String)

Definimos los codificadores y decodificadores implícitos de la siguiente manera:

 implicit val fooKeyEncoder: KeyEncoder[Foo] = new KeyEncoder[Foo] {
   override def apply(foo: Foo): String = foo.value
 }
 implicit val fooKeyDecoder: KeyDecoder[Foo] = new KeyDecoder[Foo] {
   override def apply(key: String): Option[Foo] = Some(Foo(key))
 }

Sea la siguiente estructura Map con un conjunto de claves y valores:

 val map = Map[Foo, Int](
   Foo("hola") -> 123,
   Foo("mundo") -> 456
 )

El proceso de conversión de la estructura Map en JSON, se realiza de la siguiente forma:

 val mapJson= map.asJson
 println(s"Conversión Map[Foo, Int]-> Json =${mapJson}")

La salida por consola es la siguiente:

Conversión Map[Foo, Int]-> Json ={
 "hola" : 123,
 "mundo" : 456
}

El preceso de conversión de una estructura JSON a una estructura Map, se realiza decodificando los valores de la siguiente forma:

 println(s"Conversión Json -> Map[Foo, Int] =${mapJson.as[Map[Foo, Int]]}")

La salida por consola es la siguiente:

Conversión Json -> Map[Foo, Int] =Right(Map(Foo(hola) -> 123, Foo(mundo) -> 456))

En la siguiente entrada, Circe IV: ópticas, realizaré una descripción del uso de ópticas con Circe.

Circe II: manipulación y modificación de JSON

En la entrada anterior, Circe I: introducción y parseadores, realicé una introducción de la librería Circe y, además, describí como se realiza un parseo de una estructura JSON. En la presente entrada, Circe II: manipulación y modificación de JSON, describiré cómo se puede manipular una estructura JSON.

El sketchnote de la presente entrada, queda descrito en la siguiente imagen:

Las operaciones que trataré en la entrada son para la obtención de un campo determinado, o bien, para realizar una modificación de un campo. La importación de los elementos necesarios para manipular JSON son los siguientes:

 import cats.syntax.either._
 import io.circe._
 import io.circe.parser._

La estructura JSON para las pruebas es la siguiente:

val json: String =
 """
 {
 "id": "c730433b-082c-4984-9d66-855c243266f0",
 "name": "Foo",
 "counts": [1, 2, 3],
 "values": {
 "bar": true,
 "baz": 100.001,
 "qux": ["a", "b", "c"]
 }
 }
 """

Para manipular una estructura JSON, es necesario aplicar el parseador para determinar si está bien formado; para ello, realizamos los pasos definidos en la anterior entrada. Para nuestro ejemplo, el parseo se realiza de la siguiente forma:

val doc: Json = parse(json).getOrElse(Json.Null)

Acceso a datos

Para acceder a los datos existentes en un JSON es necesario definir un cursor a partir del objeto obtenido del parseo de la estructura JSON. La definición del cursor en nuestro ejemplo es el siguiente:

val cursor: HCursor = doc.hcursor

Una vez creado el cursor, estamos en disposición para acceder a los datos, existiendo dos formas de acceso, las cuales son las siguientes:

  • La primera forma para acceder a un campo utilizamos la función downField(«NombreCampo»), para obtener del cursor el elemento referenciado para todos los campos; y, una vez posicionados en el campo seleccionado, utilizamos la función as especificando el tipo. Un ejemplo de esta forma de acceso es la siguiente:
 val valueBaz1: Decoder.Result[Double] = cursor.downField("values").downField("baz").as[Double]
 println(s"[*] Valor 'baz' del JSON forma 1=${valueBaz1}")
 println

 La salida por consola es la siguiente:

  [*] Valor 'baz' del JSON forma 1=Right(100.001)
  • La segunda forma para acceder a un campo utilizamos la función downField(«NombreCampo»), para obtener el cursor al elemento referenciado; y, con ésta referencia, utilizamos la función get especificando el tipo y el nombre del campo. Un ejemplo de esta forma de acceso es la siguiente:
 val valueBaz2: Decoder.Result[Double] = cursor.downField("values").get[Double]("baz")
 println(s"[*] Valor 'baz' del JSON forma 2=${valueBaz1}")
 println

La salida por consola es la siguiente:

[*] Valor 'baz' del JSON forma 2=Right(100.001)
  • Para acceder a una lista de elementos utilizamos la función downField(«NombreCampo») y, además, la función downArray junto a la
    función as con el tipo. Un ejemplo de acceso al primer elemento, último elemento y acceso al elemento colocado a la derecha del activo es el siguiente:
 val secondQux1: Decoder.Result[String] = cursor.downField("values").downField("qux").downArray.right.as[String]
 println(s"[*] Valor del array del campo 'qux' del JSON =${secondQux1}")
 println
 val firstQux: Decoder.Result[String] = cursor.downField("values").downField("qux").downArray.first.as[String]
 println(s"[*] Valor del array del campo 'qux' del JSON =${firstQux}")
 println
 val lastQux: Decoder.Result[String] = cursor.downField("values").downField("qux").downArray.last.as[String]
 println(s"[*] Valor del array del campo 'qux' del JSON =${lastQux}")
 println

La salida por consola es la siguiente:

[*] Valor del array del campo 'qux' del JSON =Right(b)
[*] Valor del array del campo 'qux' del JSON =Right(a)
[*] Valor del array del campo 'qux' del JSON =Right(c)

Modificación de datos

Para realizar la modificación de un campo es necesario realizar funciones parecidas al acceso de datos. Es necesario utilizar la función downField y las función withFocus. Así, unos ejemplos de modificación son los siguientes:

  •  Modificación del campo name de la estructura JSON de ejemplo asignando su valor del revés y su posterior visualización, se realiza de la siguiente forma:
 val reversedNameCursor: ACursor = cursor.downField("name").withFocus(_.mapString(_.reverse))
 val reversedName: Option[Json] = reversedNameCursor.top // Retorna todo el JSON.
 println(s"[-] Operación Reverse del JSON =${reversedName}")
 println

La salida por consola es la siguiente:

[-] Operación Reverse del JSON =Some({
 "id" : "c730433b-082c-4984-9d66-855c243266f0",
 "name" : "ooF",
 "counts" : [
 1,
 2,
 3
 ],
 "values" : {
 "bar" : true,
 "baz" : 100.001,
 "qux" : [
 "a",
 "b",
 "c"
 ]
 }
})
  • La asignación del valor «VALOR MODIFICADO» al campo name y su posterior visualización, se realiza de la siguiente forma:
 val modificacion1ameCursor: ACursor = cursor.downField("name").withFocus(_.mapString( elem => "VALOR MODIFICADO"))
 println(s"[-] Modificación del campo name del JSON =${modificacion1ameCursor.top}")
 println

La salida por consola es la siguiente:

[-] Modificación del campo name del JSON =Some({
 "id" : "c730433b-082c-4984-9d66-855c243266f0",
 "name" : "VALOR MODIFICADO",
 "counts" : [
 1,
 2,
 3
 ],
 "values" : {
 "bar" : true,
 "baz" : 100.001,
 "qux" : [
 "a",
 "b",
 "c"
 ]
 }
})

En la siguiente entrada, Circe III: encoding y decoding, realizaré una descripción de cómo realizar operaciones de codificación y decodificación de estructuras JSON con Circe.

Circe I: introducción y parseadores

En todos los sistemas informáticos es necesario el intercambio información mediante unas estructuras de datos. Una de las opciones de intercambio, es la utilización de ficheros; estos ficheros, pueden estár definidos mediante estructuras de datos tipo XML o JSON. En esta entrada, no voy a describir las ventajas de cada uno, ni en qué circunstancias hay que utilizar cada una de ellas; sólamente, me centrará en la librería Circe la cual es una librería de manipulación de estructuras de tipo JSON.

El sketchnote de la presente entrada, queda descrito en la siguiente imagen:

JSON es una acrónimo de JavaScript Object Notation. Es un formato ligero para el intercambio de datos y, debido a su amplio uso, es una alternativa a XML, considerándose como un lenguaje independiente. Un ejemplo de estructura en JSON es el siguiente:

{
 "foo": "bar",
 "baz": 123,
 "list": [ 4, 5, 6 ]
 }

El ejemplo anterior, está compuesto de una estructura formada por tres datos: foo, de tipo String; baz, de tipo entero; y, list, estructura de tipo lista de enteros.

Circe

Para la manipulación de estructuras JSON en lenguaje Scala y en Scala.js, se puede utilizar la librería Circe. La librería Circe es un fork de la librería Argonaut.

La presente entrada, Circe I: introducción y parseadores, es una introducción a la librería y una descripción de cómo se parsea una estructura JSON. En las siguientes entradas, describiré el resto de elementos de Circe.

Actualmente, Circe está en la versión 0.9.1 y tiene dependencia con Scalaz y Cats. La definición de las dependencias en sbt se realiza de la siguiente forma:

 libraryDependencies += "io.circe" %% "circe-core" % "0.9.1",
 libraryDependencies += "io.circe" %% "circe-generic" % "0.9.1",
 libraryDependencies += "io.circe" %% "circe-parser" % "0.9.1",
 libraryDependencies += "io.circe" %% "circe-optics" % "0.9.1"

Si se emplea la versión de Scala 2.10, es necesario la utilización de plugin «Paradise». La definición del plugin se realiza como sigue:

addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full)

Circe no provee ni usa lentes. Si se desea la utilización de lentes es necesario utilizar la librería Monocle. Para el lector interesado en Monocle, puede consultar las entradas que he realizado de la librería cuya primera entrada es Monocle I: introducción y lente Iso

Paseadores de JSON

Los parseadores de JSON son aquellos elementos que determinan si una entrada cumple con las reglas JSON, o bien, mostrar un mensaje informativo.

Las elementos necesarios para operar con los parseadores se encuentran en los siguientes paquetes de Circe:

 import io.circe._
 import io.circe.parser._

El juego de pruebas para los ejemplos son los siguientes:

val rawJson: String =
 """
 {
   "foo": "bar",
   "baz": 123,
   "list of stuff": [ 4, 5, 6 ]
 }
 """
 val badJson: String = "lol"

Para ejecutar los ejemplos, es necesario utilizar la función parse y, unos ejemplos de uso, son los siguientes:

 val parseResult = parse(rawJson)
 println(s"[1] Parser Json puro=${parseResult}")
 println
 // Retorno un Either: Left, con el error.
 val parseBadJson = parse(badJson)
 println(s"[2] Parse Json erróneo=${parseBadJson}")
 println

La salida por consola es la siguiente:

[1] Parser Json puro=Right({
 "foo" : "bar",
 "baz" : 123,
 "list of stuff" : [
    4,
    5,
    6
  ]
 })

[2] Parse Json erróneo=Left(io.circe.ParsingFailure: expected json value got l (line 1, column 1))

Los ejemplos anteriores, se pueden tratar como Pattern Matching de la siguiente forma:

 parse(rawJson) match {
   case Right(json) => println(s"[3] JSON válido: ${json}")
   case Left(failure) => println(s"[3] JSON no válido")
 }
 println
 parse(badJson) match {
   case Right(json) => println(s"[4] JSON válido: ${json}")
   case Left(failure) => println(s"[4] JSON no válido")
 }
 println

La salida por consola es la siguiente:

 [3] JSON válido: {
 "foo" : "bar",
 "baz" : 123,
 "list of stuff" : [
   4,
   5,
   6
 ]
 }
 [4] JSON no válido

Para controlar los valores null, utilizamos la función getOrElse de la librería cats.syntax.either de la sigueinte forma:

 import cats.syntax.either._
 val json: Json = parse(rawJson).getOrElse(Json.Null)
 println(s"[5] Ejemplo getOrElse=${json}")
 println
 val json2: Json = parse(badJson).getOrElse(Json.Null)
 println(s"[6] Ejemplo getOrElse=${json2}")
 println

La salida por consola es la sigueinte:

 [5] Ejemplo getOrElse={
 "foo" : "bar",
 "baz" : 123,
 "list of stuff" : [
   4,
   5,
   6
  ]
 }
 [6] Ejemplo getOrElse=null

En la siguiente entrada, Circe II: manipulación y modificación de JSON, realizaré una descripción de cómo manipular las estructuras JSON con Circe.

Monocle V: lente Traversal

En la entrada anterior, Monocle IV: lente Prism, realicé una descripción de la lente Prism, así como, la descripción de unos ejemplos de uso. En la presente entrada, Monocle V: lente Traversal, me centraré en la lente Traversal.

La lente Traversal es aquella lente que generaliza la lente Optional. El ejemplo común de Traversal es poner el foco en todos los elementos de un contenedor de tipo List, Vector u Option.

La lente Traversal relaciona las typeclasses Traverse y Traversal de la librería Scalaz.

Para los ejemplos, definiremos los siguientes contenedores e importaciones de elementos siguientes:

 import monocle.Traversal
 import scalaz.std.list._ 
 val xs = List(1,2,3,4,5)
 val ys = List.empty[Int]

Definición de la lente Traversal

La definición de la lente Traversal de ejemplo es la siguiente:

val eachL = Traversal.fromTraverse[List, Int]

Operaciones básicas

Operación Set

Para la asignación de un valor a todos los elementos, se utiliza la función set de la siguiente manera:

 println(s"1.- eachL.set(69)(List(1,2,3,4,5))= ${eachL.set(69)(xs)} ")
 println(s"2.- eachL.set(69)(List.empty[Int])= ${eachL.set(69)(ys)} ")

La salida por consola es la siguiente:

 1.- eachL.set(69)(List(1,2,3,4,5))= List(69, 69, 69, 69, 69) 
 2.- eachL.set(69)(List.empty[Int])= List()

Operación Get

Para la obtención de todos los elementos existentes en la lente Traversal, se utiliza la función getAll de la siguiente manera:

 println(s"5.- eachL.getAll(List(1,2,3,4,5))= ${eachL.getAll(xs)} ")
 println(s"6.- eachL.getAll(List.empty[Int])= ${eachL.getAll(ys)} ")

La salida por consola es la siguiente:

 5.- eachL.getAll(List(1,2,3,4,5))= List(1, 2, 3, 4, 5) 
 6.- eachL.getAll(List.empty[Int])= List()

Operación Modify

Para la modificación de todos los elementos de la lente Traversal, se utiliza la función modify de la siguiente manera:

 println(s"3.- eachL.modify(_ + 1)(List(1,2,3,4,5))= ${eachL.modify(_ + 1)(xs)} ")
 println(s"4.- eachL.modify(_ + 1)(List.empty[Int])= ${eachL.modify(_ + 1)(ys)} ")

La salida por consola es la siguiente:

 3.- eachL.modify(_ + 1)(List(1,2,3,4,5))= List(2, 3, 4, 5, 6) 
 4.- eachL.modify(_ + 1)(List.empty[Int])= List()

Otras operaciones

Para la obtención de la cabecera de un contenedor de una lente Traversal, se utiliza la función headOption; para buscar un elemento en función de una posición, se emplea la función find; y, para aplicar una función a todos los elementos de la lente, se emplea la función all. Unos ejemplos de las funciones descritas son los siguientes:

 println(s"5.- eachL.headOption(List(1,2,3,4,5))= ${eachL.headOption(xs)} ")
 println(s"6.- eachL.headOption(List.empty[Int])= ${eachL.headOption(ys)} ")
 println(s"7.- eachL.find(_ > 3)(List(1,2,3,4,5))= ${eachL.find(_ > 3)(xs)} ") // OJO! Retorna el elemento siguiente a 3
 println(s"8.- eachL.find(_ > 3)(List.empty[Int])= ${eachL.find(_ > 3)(ys)} ")
 println(s"9.- eachL.all(_ % 2 == 0)(List(1,2,3,4,5))= ${eachL.all(_ % 2 == 0)(xs)} ")
 println(s"10.- eachL.all(_ % 2 == 0)(List.empty[Int])= ${eachL.all(_ % 2 == 0)(ys)} ") // OJO! Retorna true y la lista es vacía.

La salida por consola es la siguiente:

 5.- eachL.headOption(List(1,2,3,4,5))= Some(1) 
 6.- eachL.headOption(List.empty[Int])= None 
 7.- eachL.find(_ > 3)(List(1,2,3,4,5))= Some(4) 
 8.- eachL.find(_ > 3)(List.empty[Int])= None 
 9.- eachL.all(_ % 2 == 0)(List(1,2,3,4,5))= false 
 10.- eachL.all(_ % 2 == 0)(List.empty[Int])= true 

Para el lector interesado, las entradas que he realizado sobre el tema son las siguientes:

 

Monocle IV: lente Prism

En la entrada anterior, Monocle III: lente Optional , realicé una descripción de la lente Optional, así como, la la descripción de unos ejemplos de uso. En la presente entrada, Monocle IV: lente Prism, me centraré en la lente Prism.

La lente Prism tiene un uso óptica para la selección de parte de un CoProducto (Suma); por ejemplo, sealed trait o Enum. La suma corresponde con la herencia de clases y objetos.

Prism tiene dos tipos de parámetros: Prism[S, A], S representa la Suma y A una parte de la suma. La lente está definida en el paquete monocle.Prism

Para los ejemplos, definiremos la siguiente jerarquía de clases y objetos

 sealed trait Json
 case object JNull extends Json
 case class JStr(v: String) extends Json
 case class JNum(v: Double) extends Json
 case class JObj(v: Map[String, Json]) extends Json

Definición de un prisma

La definición de un prisma se puede realizar de dos formas posibles: la primera, utilizando pattern matching; y, la segunda, utilizando funciones parciales. Para los dos casos, se define el tipo de entrada y el tipo de salida. Así, tenemos los siguiente ejemplos definidos, respectivamente, con pattern matching y de forma parcial.

 // Forma 1
 val jStrForma1 = Prism[Json, String]{
   case JStr(v) => Some(v)
   case _ => None
 }(JStr)
 // Forma 2
 val jStrForma2 = Prism.partial[Json, String]{case JStr(v) => v}(JStr)

Operaciones básicas

Creación de un objeto

Dada la estructura jerárquica Json definida previamente y los prismas definidos, se puede realizar la creación de los elementos de la siguiente forma descritos en los siguientes ejemplos:

 println(s"1 Json String=${jStrForma1("hello")} ")
 println(s"2 Json String=${jStrForma2("hello")} ")

La salida por consola es la siguiente:

 1 Json String=JStr(hello) 
 2 Json String=JStr(hello

Operación Set

Dada la estructura jerárquica Json definida previamente y los prismas definidos, la operación de asignación de un valor se realiza de la siguiente forma:

 println(s"3 Set Json=${jStrForma1.set("Bar")(JStr("Hello"))}")
 println(s"5 Set 'Bar' en un tipo JNum=${jStrForma1.set("Bar")(JNum(10))}")

La salida por consola es la siguiente:

 3 Set Json=JStr(Bar)
 5 Set 'Bar' en un tipo JNum=JNum(10.0)

Operación Get

Dada la estructura jerárquica Json definida previamente y los prismas definidos, la operación de obtención de un valor se realiza de la siguiente forma:

 println(s"1 Json String (JStr) a Option=${jStrForma1.getOption(JStr("Hello"))}")
 println(s"1 Json Double (JNum) a Option=${jStrForma1.getOption(JNum(3.2))}") // JNum no está definido en jStrForma1.

 println(s"2 Json String (JStr) a Option=${jStrForma2.getOption(JStr("Hello"))}")
 println(s"2 Json Double (JNum) a Option=${jStrForma2.getOption(JNum(3.2))}") // JNum no está definido en jStr.

La salida por consola es la siguiente:

 1 Json String (JStr) a Option=Some(Hello)
 1 Json Double (JNum) a Option=None

 2 Json String (JStr) a Option=Some(Hello)
 2 Json Double (JNum) a Option=None

Operación modify

Dada la estructura jerárquica Json definida previamente y los prismas definidos, la operación de modificación de un valor se realiza de la siguiente forma:

 println(s"4 Modify Json=${jStrForma1.modify(_.reverse)(JStr("Hello"))}")
 println(s"6 Modify reverse de JNum(10)=${jStrForma1.modify(_.reverse)(JNum(10))}")
 println(s"7 ModifyOption String=${jStrForma1.modifyOption(_.reverse)(JStr("Hello"))}")
 println(s"8 ModifyOption Num=${jStrForma1.modifyOption(_.reverse)(JNum(10))}")

La salida por consola es la siguiente:

 4 Modify Json=JStr(olleH)
 6 Modify reverse de JNum(10)=JNum(10.0)
 7 ModifyOption String=Some(JStr(olleH))
 8 ModifyOption Num=None

Composición de prismas

Monocle tiene definidos prismas de tipos básicos, como por ejemplo: double, bigInt, bigDecimal,…definidos en el paquete monocle.std.xxx

Con los primas existentes y con los prismas que definimos, podemos realizar composición de los mismos con la función composePrism. En el siguiente ejemplo, defino dos primas, siendo uno de ellos, una composición de un prisma que definido;y, además, el prisma de transformación de un elemento de tipo Double y un entero.

 import monocle.std.double.doubleToInt // Prism[Double, Int] defined in Monocle
 val jNum: Prism[Json, Double] = Prism.partial[Json, Double]{case JNum(v) => v}(JNum)
 val jInt: Prism[Json, Int] = jNum composePrism doubleToInt
 println(s"9 Entero =${jInt(5)} ")
 println(s"9 Entero con Option=${jInt.getOption(JNum(5.0))}")
 println(s"9 Double con Option=${jInt.getOption(JNum(5.2))}")
 println(s"9 String con Option=${jInt.getOption(JStr("Hello"))}")

La salida por consola es la siguiente:

 9 Entero =JNum(5.0) 
 9 Entero con Option=Some(5)
 9 Double con Option=None
 9 String con Option=None

Generadores de Prismas

Los generadores de prismas son macros existentes que facilitan la creación de Prismas. Los generadores de prismas están en el paquete monocle.macros.GenPrism. Así, la definición de un prisma mediante un generador, se realiza de la siguiente forma:

 import monocle.Prism
 import monocle.macros.GenPrism
 val rawJNum: Prism[Json, JNum] = GenPrism[Json, JNum]

Para obtener un valor utilizando el prisma anterior, se realiza de la siguiente forma:

 println(s"1 GenPrism JNum(10.0)=${rawJNum.getOption(JNum(10.0))}")
 println(s"2 GenPrism JStr('Prueba')=${rawJNum.getOption(JStr("Prueba"))}")

La salida por consola es la siguiente:

 1 GenPrism JNum(10.0)=Some(JNum(10.0))
 2 GenPrism JStr('Prueba')=None

Además de los prismas, podemos utilizar otras lentes, como por ejemplo el generador de la lente Iso. En el siguiente ejemplo, se muestra un ejemplo de uso de las lentes prisma e Iso utilizando composición de lentes:

 import monocle.macros.GenIso
 val jNum: Prism[Json, Double] = GenPrism[Json, JNum] composeIso GenIso[JNum, Double]
 val jNull: Prism[Json, Unit] = GenPrism[Json, JNull.type] composeIso GenIso.unit[JNull.type]
 println(s"3 GenPrism-GenIso=${jNum.getOption(JNum(10.0))}")
 println(s"4 GenPrism-GenIso=${jNull.getOption(JNum(10.0))}")
 println(s"5 GenPrism-GenIso=${jNum.getOrModify(JNum(10.0))}")
 println(s"6 GenPrism-GenIso=${jNum.getOrModify(JNum(10.0)).getOrElse(0.0)}")

La salida por consola es la siguiente:

 3 GenPrism-GenIso=Some(10.0)
 4 GenPrism-GenIso=None
 5 GenPrism-GenIso=\/-(10.0)
 6 GenPrism-GenIso=10.0

En la siguiente entrada, Monocle V: lente Travesal, describiremos la lente Traversal de la librería Monocle así como unos ejemplos prácticos.

Para el lector interesado, las entradas que he realizado sobre el tema son las siguientes:

Monocle III: lente Optional

En la entrada anterior, Monocle II: lente Lens, realicé una descripción de la lente Lens, así como, la descripción de unos ejemplos de uso. En la presente entrada, Monocle III: lente Optional, me centraré en la lente Optional.

La lente Optional tiene un uso óptico para realizar un zoom en un producto; por ejemplo, una case class o un Map. La lente Optional tiene un uso óptico parecido a la lente Lens; pero, la diferencia, reside en que el elemento en el que se focaliza la operación puede no existir.

Optional tiene dos tipos de parámetros, llamados S y A: Optional[S, A], S representa el producto y A un elemento optinal de S. La lente está definida en el paquete monocle.Optional.

Para los ejemplos, utilizaremos una colección de enteros, representados en una lista, y una lente Optional para dicha colección. Así, definimos una lente Optional para la obtención de la cabeza de la lista de la siguiente manera:

val head = Optional[List[Int], Int] {
   // get
   case Nil => None
   case x :: xs => Some(x)
 } {
   // set
   a => {
     case Nil => Nil
     case x :: xs => a :: xs
   }
 }

La definición de la lente tiene la siguiente estructura: la primera parte, define la función get; y, la segunda, define la función set.

Operaciones básicas

Operación nonEmpty

Para determinar si una lista está vacía o no, se utiliza la función nonEmpty de la forma descrita en los siguientes ejemplos:

 println(s"1.- head.nonEmpty( List(1, 2, 3) )=${head.nonEmpty(xs)}")
 println(s"2.- head.nonEmpty( List.empty[Int] )=${head.nonEmpty(ys)}")

La salida por consola es la siguiente:

 1.- head.nonEmpty( List(1, 2, 3) )=true
 2.- head.nonEmpty( List.empty[Int] )=false

Operación Get

Para obtener el valor de la lente, se utiliza la función get de la forma descrita en los siguientes ejemplos:

 println(s"3.- head.getOrModify( List(1, 2, 3) )=${head.getOrModify(xs)}")
 println
 println(s"3.1.- head.getOrModify( List(1, 2, 3) ).getOrElse(0)=${head.getOrModify(xs).getOrElse(0)}")
 println
 println(s"4.- head.getOrModify( List.empty[Int] )=${head.getOrModify(ys)}")
 println
 println(s"4.1.- head.getOrModify( List.empty[Int] ).getOrElse(0)=${head.getOrModify(ys).getOrElse(0)}")
 println
 println(s"5.- head.getOption( List(1, 2, 3) )=${head.getOption(xs)}")
 println
 println(s"6.- head.getOption( List.empty[Int] )=${head.getOption(ys)}")
 println

La salida por consola es la siguiente:

 3.- head.getOrModify( List(1, 2, 3) )=\/-(1)
 3.1.- head.getOrModify( List(1, 2, 3) ).getOrElse(0)=1
 4.- {head.getOrModify( List.empty[Int] )=-\/(List())
 4.1.- head.getOrModify( List.empty[Int] ).getOrElse(0)=0
 5.- head.getOption( List(1, 2, 3) )=Some(1)
 6.- head.getOption( List.empty[Int] )=None

La función getOrModify es utiliza normalmente para polimorfísmo ópticos; y, getOption, es utilizada para monomorfirmos.

Operación Set

Para asignar un valor a la lente, se emplea la función set de la forma descrita en los siguiente ejemplos:

 println(s"7.- head.set(69)( List(1, 2, 3) )=${head.set(69)(xs)}")
 println
 println(s"8.- head.set(69)( List.empty[Int] )=${head.set(69)(ys)}")
 println
 println(s"13.- head.setOption(69)( List(1, 2, 3) )=${head.setOption(69)(xs)}")
 println
 println(s"14.- head.setOption(69)( List.empty[Int] )=${head.setOption(69)(ys)}")
 println

La salida por consola es la siguiente:

 7.- head.set(69)( List(1, 2, 3) )=List(69, 2, 3)
 8.- head.set(69)( List.empty[Int] )=List() 
 13.- head.setOption(69)( List(1, 2, 3) )=Some(List(69, 2, 3))
 14.- head.setOption(69)( List.empty[Int] )=None

Operación modificación

Para realizar una modificación de la lente, se emplea la función modify de la forma descrita en los siguiente ejemplos:

 println(s"9.- head.modify(_ + 1)( List(1, 2, 3) )=${head.modify(_ + 1)(xs)}") 
 println
 println(s"10.- head.modify(_ + 1)( List.empty[Int] )=${head.modify(_ + 1)(ys)}")
 println
 println(s"11.- head.modifyOption(_ + 1)( List(1, 2, 3) )=${head.modifyOption(_ + 1)(xs)}")
 println
 println(s"12.- head.modifyOption(_ + 1)( List.empty[Int] )=${head.modifyOption(_ + 1)(ys)}")
 println

La salida por consola es la siguiente:

 9.- head.modify(_ + 1)( List(1, 2, 3) )=List(2, 2, 3)
 10.- head.modify(_ + 1)( List.empty[Int] )=List()
 11.- head.modifyOption(_ + 1)( List(1, 2, 3) )=Some(List(2, 2, 3))
 12.- head.modifyOption(_ + 1)( List.empty[Int] )=None

En la siguiente entrada, Monocle IV: lente Prism, describiremos la lente Prism de la librería Monocle así como unos ejemplos prácticos.

Para el lector interesado, las entradas que he realizado sobre el tema son las siguientes: