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:

Monocle II: lente Lens.

En la entrada anterior, Monocle I: introducción y lente Iso, presenté la libreía Monocle y realicé una descripción de la óptica Iso con ejemplos prácticos. En la presente entrada, Monocle: óptica lens, realizaré la definición y descripción de la lente Lens.

La óptica Lens es aquella lente que realiza un zoom para aquella operación de Producto. Lens tiene dos tipos de parámetros S y A: Lens[S, A], donde S es el producto y A el elemento dentro de la estructura S. En una case class, la operación producto es la que se define con los parámetros que se pasan a la clase.

Para los ejemplos de los siguientes apartados, definiré una estructuda de case clases las cuáles representan una entidad de dominio de un caso de uso determinado.

Las entidades a definir representan la entidad persona y su dirección. Así, la definición de las case class es la siguiente:

 case class Direccion(numeroCalle: Int, nombreCalle: String)
 case class Persona(nombre: String, age: Int, address: Direccion)

Operaciones básicas

Dada las entidades de dominio, definimos la óptica Lens del campo numeroCalle de la entidad Dirección de la siguiente forma:

 import monocle.macros.GenLens
 val dirección = Direccion(1,"dirección")
 val numeroCalle = GenLens[Direccion](_.numeroCalle)
  • Realizamos la operación de obtención de un valor, get, de la siguiente forma:
println(s"Get numeroCalle=${numeroCalle.get(dirección)}")
  • Definimos la operación de asignación, set, de un valor de la siguiente forma:
val direccion2 = numeroCalle.set(5)(dirección)
  • Definimos la operación de modificación, modify, de un valor de la siguiente forma:
val direccion3 = numeroCalle.modify(_ + 1)(dirección)

La definición de modificación se puede definir combinando la operación get y set.

  • Podemos realizar operaciones de modificación de forma polimórfica con la función modifyF. El siguiente ejemplo realiza la creación de una lista de objetos Dirección con los valores vecinos de una dirección determinada.
 import scalaz.std.list._
 def vecinos(n: Int): List[Int] =
 if(n > 0) List(n - 1, n + 1)
 else List(n + 1)
 val direccion = Direccion(2,"direccion") 
 val direccion1 = numeroCalle.modifyF(vecinos)(direccion)
 println(s"modifyF1=${direccion1}")
 println

La salida por consola es la siguiente:

modifyF1=List(Direccion(1,direccion), Direccion(3,direccion))

Como observamos en el ejemplo, el valor entero corresponde con el valor entero de la entidad Dirección.

Para el caso de un valor negativo del campo entero, el ejemplo es el siguiente:

 val direccion1_2 = numeroCalle.modifyF(vecinos)( Direccion(-5,"direccion") )
 println(s"modifyF1_2=$direccion1_2}")
 println

La salida por consola es la siguiente:

modifyF1_2=List(Direccion(-4,direccion))}
  • Las operaciones set/get para campos anidados son los siguientes:
 val direccionPerson = Direccion(60,"direccion60Pepe")
 val pepe = Persona("Pepe", 20, direccionPerson)
 val direccion4 = GenLens[Persona](_.direccion)
 val direccionPepe = (direccion4 composeLens numeroCalle).get(pepe)
 println(s"direccionPepe=${direccionPepe}")
 println
 // Cambio de 60 a 2.
 val direccion5 = (direccion4 composeLens numeroCalle).set(2)(pepe)
 println(s"direccion5=${direccion5}")
 println

La salida por consola es la siguiente:

 direccionPepe=60
 direccion5=Persona(Pepe,20,Direccion(2,direccion60Pepe))

Anotación @Lenses

La anotación @Lenses permite la creación de ópticas para todos los campos de la entidad. Así, en el siguiente ejemplo, realizamos la creación de una entidad con nombre Punto para realizar las operaciones get/set de sus atributos.

 import monocle.macros.Lenses
 @Lenses case class Punto(x: Int, y: Int)
 val p = Punto(5, 3)
 val pPoint = Punto.x.get(p)
 println(s"Valor x de Punto(5,3)=${pPoint}")
 println
 println(s"Valor y de Punto(5,3)=${Punto.y.get(p)}")
 println

La salida por consola es la siguiente:

Valor x de Point(5,3)=5
Valor y de Point(5,3)=3

Operaciones avanzadas

Sean las entidades de dominio Juego, Nivel y Pantalla definidas con la anotación @Lenses de la siguiente forma:

 @Lenses case class Pantalla(ancho: Int, alto: Int)
 @Lenses case class Nivel(puntosMaximos: Int, pantalla: Pantalla)
 @Lenses case class Juego(jugadores: Int, nivel: Nivel)
 val juegoPrueba1 = Juego(2, Nivel(100, Pantalla(30, 50)))

Supongamos que queramos realizar la modificación de los campos ancho y alto de la pantalla sin aplicar lentes. La solución podría ser de la siguiente forma:

 def cambiarPantalla(juego: Juego)(anchoNuevo: Int, altoNuevo: Int): Juego =
 juego.copy(nivel = juego.nivel.copy(pantalla = juego.nivel.pantalla.copy(ancho = anchoNuevo, alto = altoNuevo)))
 println(s"Modificación del Juego de prueba= ${cambiarPantalla(juegoPrueba1)(100, 100)} ")

La salida por pantalla sería lo siguiente:

Modificación del Juego de prueba= Juego(2,Nivel(100,Pantalla(100,100)))

Como podemos ver en el ejemplo, para realizar un cambio en un campo tenemos que realizar un trabajo tedioso para realizar una operación muy sencilla.

Mediante la utilización de lentes, las operaciones básicas las podemos realizar de una forma sencilla mediante la definición de lentes. Para las entidades Juego, Nivel y Pantalla podemos definir las siguiente lentes:

 val lensJuego : Lens[Juego, Nivel] = GenLens[Juego]( _.nivel)
 val lensJugadoresJuego: Lens[Juego, Int] = GenLens[Juego](_.jugadores)
 val lensNivel : Lens[Nivel, Pantalla] = GenLens[Nivel]( _.pantalla)
 val lensNivelPuntosMaximos : Lens[Nivel, Int] = GenLens[Nivel](_.puntosMaximos)
 val lensAnchoPantalla : Lens[Pantalla, Int] = GenLens[Pantalla](_.ancho)
 val lensAltoPantalla : Lens[Pantalla, Int] = GenLens[Pantalla](_.alto)
  • Para obtener el alto y ancho de la pantalla de la clase juegoPrueba1, se realiza de la siguiente forma:
 println(s" Alto pantalla de juegoPrueba1 =${(lensJuego ^|-> lensNivel ^|-> lensAltoPantalla ).get(juegoPrueba1)}")
 println(s" Ancho pantalla de juegoPrueba1 =${(lensJuego ^|-> lensNivel ^|-> lensAnchoPantalla ).get(juegoPrueba1)}")

La salida por consola es la siguiente:

 Alto pantalla de juegoPrueba1 =50
 Ancho pantalla de juegoPrueba1 =30
  • Utilizando la funcionalidad de las lentes, las operaciones de set y get se pueden realizar de la siguiente forma:
 println(s"Modificacion número jugadores=${Juego.jugadores.set(69)(juegoPrueba1)} ")
 println(s"Alto de pantallla de juegoPrueba1=${Juego.nivel.get(juegoPrueba1).pantalla.alto } ")
 println(s"Ancho de pantallla de juegoPrueba1=${Juego.nivel.get(juegoPrueba1).pantalla.ancho } ")
 println(s"puntosMaximos de nivel de juegoPrueba1=${Juego.nivel.get(juegoPrueba1).puntosMaximos } ")

La salida por consola es la siguiente:

 Modificacion número jugadores=Juego(69,Nivel(100,Pantalla(30,50))) 
 Alto de pantallla de juegoPrueba1=50 
 Ancho de pantallla de juegoPrueba1=30 
 puntosMaximos de nivel de juegoPrueba1=100

En la siguiente entrada, Monocle III: lente Optional, describiremos la lente Optional 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 I: introducción y lente Iso

Inicio una serie de cinco entradas cuyo tema principal son las ópticas en Scala y, en concreto, las ópticas definidas en la librería Monocle.

En la presente entrada, Monocle I: introducción y lente Iso, me centraré en realizar una presentación genérica y en describir unas de las lentes existentes en Monocle, la lente Iso. En las siguientes entradas de la serie, describiré el resto de lentes.

Presentación del problema

En Scala podemos definir las entidades de dominio con case class. Una case class es una clase con los métodos set y get, entre otros, de los atributos que contiene. Supongamos las siguientes entidades de dominio:

 case class Street(number: Int, name: String, ciudad:String)
 case class Address(city: String, street: Street)
 case class Company(name: String, address: Address)
 case class Employee(name: String, company: Company)

La creación de una entidad Employee puede ser como sigue:

val empleado = Employee("alvaro", Company("mi empresa", Address("ciudad", Street(15, "calle", "MD"))))

Si queremos modificar el nombre de la calle del empleado, tendremos que realizar algo como lo siguiente:

val employee2 = employee.copy(
  company = employee.company.copy(
   address = employee.company.address.copy(
    street = employee.company.address.street.copy(
     name = employee.company.address.street.name.capitalize // Pone la primera en mayúscula
    )
   )
  )
 )

Como observamos, el proceso de copiado/modificación es muy laborioso; y, estando con un lenguaje funcional, la solución a este problema se resuelve con una librería óptica.

Definición conceptual de lente

Una lente tiene que cumplir unas características:

  1. Paramétrica.- Una lenta debe de especificar el tipo del objeto de la lente.
  2. Una lente por campo.- Se debe de definir una lente por cada campo que queremos tratar.
  3. Getter.- Se debe de definir el mecanismo para obtener los valores de los atributos de las entidades.
  4. Setter.- Se debe de definir el mecanismo para asignar un nuevo valor a los atributos de las entidades.

Desde un punto de vista genérico, podemos definir una lente de la siguiente manera:

case class Lente[O, V](
 get: O => V,
 set: (O, V) => O
)

Configuración en sbt

Para utilizar la librería Monocle es necesario realizar la definición de las dependencias de la librería. Así, la definición de las dependencias en el fichero build.sbt es la siguiente:

 libraryDependencies += "com.github.julien-truffaut" %% "monocle-core" % monocleVersion,
 libraryDependencies += "com.github.julien-truffaut" %% "monocle-macro" % monocleVersion,
 libraryDependencies += "com.github.julien-truffaut" %% "monocle-law" % monocleVersion % "test"

Por otro lado, si trabajamos con una versión de Scala inferior a 2.12, y para poder utilizar macros y anotaciones de Monocle, es necesario definir en el fichero build.sbt el siguiente plugin:

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

Lente Iso

La lente Iso es aquella óptica que convierte elementos de tipo S dentro de elementos de tipo A sin perder campos; por ejemplo, la conversión de una lista de enteros en un vector de enteros.

Para los ejemplos, definimos las siguientes entidades de dominio:

 case class Persona(nombre: String, edad: Int)
 case class MyString(s: String)
 case class Foo()
 case object Bar
 case class MyListInt(miLista:List[Int])

Ejemplo con case class y tuplas.

Definimos la lente personaATupla de tipo Iso para la transformación de la entidad Persona en una tupla con el nombre y edad de la persona a transformar.

 import monocle.Iso
 val personaATupla = Iso[Persona, (String, Int)] 
 ( p => (p.nombre, p.edad) ) // [A]
 { case (name, age) => Persona(name, age) } // [B]

La lente Iso define una función get y una función reverse del get. La función get es aquella función que dada una entidad Persona, realiza la creación de la tupla, en nuestro ejemplo, la identificada con [A];y, la función reverseGet, es aquella función que dada una tupla realiza la creación de la entidad Persona, en nuestro ejemplo, la identificada con [B]. A continuación, se muestran unos ejemplos de transformación:

println(personToTuple.get(Persona("Zoe", 25)))

La salida por consola es la siguiente:

(Zoe,25)

El siguiente snippet realiza la transformación de una tupla a la entidad Persona:

println(personToTuple.reverseGet(("Zoe", 25)))

La salida por consola es la siguiente:

Persona(Zoe,25)

Ejemplo con List y Vector

Definición de una lente para la transformación de una List de tipo A a un Vector de tipo A y una lente con la función contraria. A continuación, se muestran unos ejemplos de transformación:

  import monocle.Iso
  def listToVector[A] = Iso[List[A], Vector[A]](_.toVector)(_.toList)
  def vectorToList[A] = listToVector[A].reverse
  println(s"List a Vector=${listToVector.get(List(1,2,3))}") 
  println(s"List a Vector=${listToVector.get(List("'a'","'b'","'c'"))}")
  println(s"Vector a List= ${vectorToList.get(Vector(1,2,3))}")
  println(s"Vector a List= ${vectorToList.get(Vector("'a'","'b'","'c'"))}")

La salida por consola es la siguiente:

 List a Vector=Vector(1, 2, 3)
 List a Vector=Vector('a', 'b', 'c')
 Vector a List= List(1, 2, 3)
 Vector a List= List('a', 'b', 'c')

Ejemplo con String

Definición de una lente de conversión de un String a una lista de Char y su función contraria. A continuación, se muestran unos ejemplos de transformación:

  import monocle.Iso
  val stringToList = Iso[String, List[Char]](_.toList)(_.mkString(""))
  println(s"stringToList.get('Hello')=${stringToList.get("Hello")}")
  println(s"stringToList.reverseGet(List('a','b','c'))=${stringToList.reverseGet(List('a','b','c'))}

La salida por consola es la siguiente:

 stringToList.get('Hello')=List(H, e, l, l, o)
 stringToList.reverseGet(List('a','b','c'))=abc

Macros GenIso

Para facilitar la generación de la lente Iso entre case class y tuplas, se define la macro GenIso definida en el paquete monocle.macros.Iso.

Unos ejemplos de la macro GenIso son los siguientes:

 println(s"Generacion MyString a String=${GenIso[MyString, String].get(MyString("Hello"))}")
 println(s"Generacion MyListInt a List[Int]=${GenIso[MyListInt, List[Int]].get(MyListInt( (1 to 5).toList ))}")
 println(s"Tupla de la case class Person=${GenIso.fields[Persona].get(Persona("John", 42))}")

Las salida por consola es la siguiente:

 Generacion MyString a String=Hello
 Generacion MyListInt a List[Int]=List(1, 2, 3, 4, 5)
 Tupla de la case class Person=(John,42)

En la siguiente entrada, Monocle II: lente Lens, describiremos la lente Len en 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:

Presentación

En el año 2011, comienzo mis primeras experiencias como  bloguero con el blog directoandroid.es. El desconocimiento del medio, cómo transmitir, estructura de las entradas, estilo de escritura, fueron aspectos que tuve que aprender y refinar. Tras las primeras entradas, el proceso fue fluido ya que el tema principal eran todas aquellas tecnologías del entorno Android que en ese momento probaba.

.

Pasados unos años, la rutina, el tipo de entrada y la inquietud de escribir sobre otros temas, me llevó a tomar la decisión de realizar cambios. Dicha inquietud, se reflejo en un nuevo blog más personal sin centrarme en un único tema, sino en tratar todas aquellas tecnologías con las que trabajaba o experimentaba. Así, nació en 2015 la fase I del blog personal en donde escribí, básicamente,  sobre tecnologías como Scala, Spark, Kafka, programación funcional. A finales del 2017, por circunstancias que no quiero recordar, supusieron la pérdida del blog; y, tras una fase de reflexión, comienzo la actual fase la cual espero disfrutar como todas las anteriores.