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:

 

Deja una respuesta

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

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s