Translate

miércoles, 20 de abril de 2022

Funtores en Cats parte 5

Podemos pensar en el método map de Functor como "agregar" una transformación a una cadena. Ahora vamos a ver otras dos type class, una que representa anteponer operaciones a una cadena y otra que representa la construcción de una cadena bidireccional de operaciones. Estos se denominan funtores contravariantes e invariantes respectivamente.

La primera de nuestras clases de tipos, el funtor contravariante, proporciona una operación llamada contramapa que representa "anteponer" una operación a una cadena. 

El método contramap solo tiene sentido para tipos de datos que representan transformaciones. Por ejemplo, no podemos definir un contramapa para una opción porque no hay forma de retroalimentar un valor en un Option[B] a través de una función A => B. Sin embargo, podemos definir un contramapa para la clase de tipo Imprimible :

trait Printable[A] {

  def format(value: A): String

}

Un Printable[A] representa una transformación de A a String. Su método contramapa acepta una función func de tipo B => A y crea un nuevo Imprimible[B]:

trait Printable[A] {
  def format(value: A): String
  def contramap[B](func: B => A): Printable[B] = ???
}

def format[A](value: A)(implicit p: Printable[A]): String = p.format(value)

Los funtores invariantes implementan un método llamado imap que es informalmente equivalente a una combinación de map y contramap. Si map genera nuevas instancias de clase de tipo agregando una función a una cadena y contramap las genera anteponiendo una operación a una cadena, imap las genera a través de un par de transformaciones bidireccionales.

Los ejemplos más intuitivos de esto son una clase de tipo que representa la codificación y decodificación como algún tipo de datos, como el formato Play JSON y el códec de scodec. Podemos crear nuestro propio códec mejorando Imprimible para admitir la codificación y decodificación hacia/desde una cadena:

trait Codec[A] {
def encode(value: A): String
def decode(value: String): A
def imap[B](dec: A => B, enc: B => A): Codec[B] = ???
}
def encode[A](value: A)(implicit c: Codec[A]): String =
c.encode(value)
def decode[A](value: String)(implicit c: Codec[A]): A =
c.decode(value)

Si tenemos un Codec[A] y un par de funciones A => B y B => A, el método imap crea un Codec[B]:
Como ejemplo de caso de uso, imagina que tenemos un Codec[String] básico, cuyos métodos de codificación y decodificación simplemente devuelven el valor que se les pasa:

implicit val stringCodec: Codec[String] =
new Codec[String] {
def encode(value: String): String = value
def decode(value: String): String = value
}

Podemos construir muchos códecs útiles para otros tipos construyendo a partir de stringCodec usando imap:

implicit val intCodec: Codec[Int] =
stringCodec.imap(_.toInt, _.toString)
implicit val booleanCodec: Codec[Boolean] =
stringCodec.imap(_.toBoolean, _.toString)