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:

Deja un comentario