Translate

jueves, 10 de marzo de 2022

Empezando con Cats parte 3


 Al trabajar con type class, debemos tener en cuenta dos cuestiones que controlan la selección de instancias:

  • ¿Cuál es la relación entre una instancia definida en un tipo y sus subtipos? Por ejemplo, si definimos un JsonWriter[Option[Int]], ¿la expresión Json.toJson(Some(1)) seleccionará esta instancia? (Recuerde que Some es un subtipo de Option).
  • ¿Cómo elegimos entre instancias de type class cuando hay muchas disponibles? ¿Qué pasa si definimos dos JsonWriters para Person? Cuando escribimos Json.toJson(aPerson), ¿qué instancia se selecciona?

Cuando definimos clases de tipo, podemos agregar anotaciones de variación al parámetro de tipo para afectar la variación de la clase de tipo y la capacidad del compilador para seleccionar instancias durante la resolución implícita.

La varianza se relaciona con los subtipos. Decimos que B es un subtipo de A si podemos usar un valor de tipo B en cualquier lugar donde esperamos un valor de tipo A.

Las anotaciones de covarianza y contravarianza surgen cuando se trabaja con constructores de tipos. Por ejemplo, denotamos la covarianza con un símbolo +:

trait F[+A] // the "+" means "covariant"

Covarianza significa que el tipo F[B] es un subtipo del tipo F[A] si B es un subtipo de A. Esto es útil para modelar muchos tipos, incluidas colecciones como List y Option:

traittraitList[+A]
Option[+A]

La covarianza de las colecciones de Scala nos permite sustituir colecciones de un tipo por una colección de un subtipo en nuestro código. Por ejemplo, podemos usar una Lista[Círculo] en cualquier lugar donde esperemos una Lista[Forma] porque Círculo es un subtipo de Forma:

sealed trait Shape
case class Circle(radius: Double) extends Shape
val circles: List[Circle] = ???
val shapes: List[Shape] = circles

En términos generales, la covarianza se usa para las salidas: datos que luego podemos obtener de un tipo de contenedor como List, o de otra manera devueltos por algún método.

¿Qué pasa con la contravarianza? Escribimos constructores de tipos contravariantes con un símbolo - como este:

trait F[-A]

La contravarianza significa que el tipo F[B] es un subtipo de F[A] si A es un subtipo de B. Esto es útil para modelar tipos que representan entradas, como nuestra clase de tipo JsonWriter anterior:

trait JsonWriter[-A] {
def write(value: A): Json
}

La varianza tiene que ver con la capacidad de sustituir un valor por otro. Considere un escenario en el que tenemos dos valores, uno de tipo Forma y otro de tipo Círculo, y dos JsonWriters, uno para Forma y otro para Círculo:

val shape: Shape = ???
val circle: Circle = ???
val shapeWriter: JsonWriter[Shape] = ???
val circleWriter: JsonWriter[Circle] = ???
def format[A](value: A, writer: JsonWriter[A]): Json = writer.write(value)

¿Qué combinaciones de value y JsonWriter pueden pasar a format? Podemos escribir un Círculo con cualquier escritor porque todos los Círculos son Formas. Por el contrario, no podemos escribir una Forma con circleWriter porque no todas las Formas son Círculos. Esta relación es lo que modelamos formalmente usando contravarianza.

JsonWriter[Shape] es un subtipo de JsonWriter[Circle] porque Circle es un subtipo de Shape. Esto significa que podemos usar shapeWriter en cualquier lugar donde esperemos ver un JsonWriter[Circle].

La invariancia es la situación más fácil de describir. Es lo que obtenemos cuando no escribimos un + o - en un constructor de tipos:

trait F[A]

Esto significa que los tipos F[A] y F[B] nunca son subtipos entre sí, independientemente de la relación entre A y B. Esta es la semántica predeterminada para los constructores de tipos de Scala.

Cuando el compilador busca un implícito, busca uno que coincida con el tipo o subtipo. Por lo tanto, podemos usar anotaciones de varianza para controlar la selección de instancias de clase de tipo hasta cierto punto.

Hay dos problemas que tienden a surgir. Imaginemos que tenemos un tipo de dato algebraico como:

sealed trait A
final case object B extends A
final case object C extends A

Los problemas son:
¿Se seleccionará una instancia definida en un supertipo si hay una disponible? Por ejemplo, ¿podemos definir una instancia para A y hacer que funcione para valores de tipo B y C?
¿Se seleccionará una instancia para un subtipo con preferencia a la de un supertipo? Por ejemplo, si definimos una instancia para A y B, y tenemos un valor de tipo B, ¿se seleccionará la instancia de B con preferencia a A?

Está claro que no existe un sistema perfecto. Cats prefiere usar clases de tipos invariantes.
Esto nos permite especificar instancias más específicas para subtipos si queremos. Significa que si tenemos, por ejemplo, un valor de tipo Some[Int], no se usará nuestra instancia de clase de tipo para Option. Podemos resolver este problema con una anotación de tipo como Some(1) : Option[Int] o usando "constructores inteligentes" como los métodos Option.apply, Option.empty, some y none.