En el artículo anterior, vimos Scala Type bounds, en el de hoy continuaremos con Scala generics y hablaremos sobre la Covarianza y Contravarianza en los tipos genéricos.

El principio de sustitución de Liskov (la L. de S.O.L.I.D.) precisa que, en una relacion de herencia, un tipo definido como supertipo tiene que estar en un contexto que permita ser sustituid por cualquiera de sus clases derivadas en cualquier momento.

Para ilustrarlo usaremos este conjunto de clases.

sealed abstract class Vehicle
case class Car() extends Vehicle
case class Motorcycle() extends Vehicle

en todo momento, definiendo:

val vehicleIdentity = (vehicle:Vehicle) => vehicle

tiene que considerarse correcto invocar tanto:

vehicleIdentity(Car())

como

vehicleIdentity(Motorcycle())

Aquí pueden encontrar más información sobre Liskov.

En los tipos genéricos, la varianza es la correlación que hay entre la relación de herencia de los tipos abstractos y cómo esta se “transmite” a la herencia en las clases genéricas.
En otras palabras, dado una class Thing[A]:
Si A hereda de B (A <: B), podemos decir que Thing[A] <: Thing[B] ?
La varianza modela esta correlación y nos permite crear clases genéricas más reusables.

Hay cuatro tipos de varianza: covarianza, contravarianza, invarianza y bivarianza,  tres de los cuales se plasman en Scala:

> Invarianza: class Parking[A]

Una clase genérica invariante sobre su tipo abstracto solo puede recibir un parameter type de exactamente ese tipo.

case class Parking[A](value: A) val p1: Parking[Vehicle] = Parking[Car](new Car) 
 <console>:12: error: type mismatch;
 found : Parking[Car]
 required: Parking[Vehicle]
 Note: Car <: Vehicle, but class Parking is invariant in type A.
 You may wish to define A as +A instead. (SLS 4.5)
 val p1: Parking[Vehicle] = Parking[Car](new Car)

El error lo deja claro, aunque Car <: Vehicle, debido a que Parking es invariante sobre A, Parking[Car] !<: Parking[Vehicle].


Sin embargo una vez definido el tipo abstracto, se puede usar dentro de la clase libremente, aplicando el principio de sustitución de Liskov:

val p1 = Parking[Vehicle](new Motorcycle)
 res8: Parking[Vehicle] = Parking(Motorcycle())
p1.value.getClass
 res9: Class[_ <: Vehicle] = class Motorcycle

> Covarianza: class Parking[+A]

Una clase genérica covariante sobre su tipo abstracto puede recibir un parameter type de ese tipo o subtipos de ese tipo.

case class Parking[+A](value: A) 
val p1: Parking[Vehicle] = Parking[Car](new Car)
p1: Parking[Vehicle] = Parking(Car())

La covarianza permite tipar p1 como Parking[Vehicle] y asignarle un Parking[Car].
Pero no hay que olvidar que aunque p1 esté tipado como Parking[Vehicle], en realidad es un Parking[Car],
esto puede ser confuso, pero más abajo explico que son las posiciones co/contravariantes y se entiende todo.

resumiendo:

For Parking[+A]
If Car <: Vehicle
Then Parking[Car] <: Parking[Vehicle]


> Contravarianza: class Parking[-A]


Una clase genérica contravariante sobre su tipo abstracto puede recibir un parameter type de ese tipo o supertipos de ese tipo.

case class Parking[-A] 
val p1: Parking[Car] = Parking[Vehicle]
p1: Parking[Car] = Parking()

La contravarianza nos permite tipar p1 como Parking[Car] y asignarle un Parking[Vehicle]

Resumiendo:

For Parking[-A]
If Vehicle >:(supertype of) Car
Then Parking[Vehicle] <: Parking[Car]

 

Posiciones Covariantes y Contravariantes

Un tipo puede estar en posición coovariante o contravariante según dónde esté especificado. Algunos de los ejemplos más claros son los siguientes:

class Pets[+A](val pet:A) {
def add(pet2: A): String = "done"
}
<console>:9: error: covariant type A occurs in contravariant position in type A of value pet2

Qué ha pasado? Hemos tipado como A(un tipo covariante debido a que Pets[+A]) el parámetro de entrada del método add().

El compilador se ha quejado, dice que el el parámetro de entrada de add está en posición contravariante, que A no es válido. ¿Por qué?

Porque si hago:

 val mascotas: Pets[Animal] = Pets[Cat](new Cat) 

Aunque mascotas esté tipado como Pets[Animal] en realidad es un Pets[Cat], por tanto debido a que mascotas está tipado como Pets[Animal] mascotas.add() aceptará Animal o cualquier subtipo de Animal, pero esto no tiene sentido, ya que en realidad mascota es un Pets[Cat] y add() solo puede aceptar Cats o subtipo de Cat.

Es decir, el compilador evita que caigamos en el absurdo de llamar a mascotas.add(Dog()), ya que es un conjunto de Cat.

Otro ejemplo, en el caso de la contravarianza es el siguiente:

class Pets[-A](val pet:A)
 <console>:7: error: contravariant type A occurs in covariant position in type => A of value pet
 class Pets[-A](val pet:A)

Qué ha pasado? Hemos tipado el parámetro de entrada como A(que es contravariante debido a que Pets[-A]) y el compilador nos ha dicho que ese parámetro está en posición covariante, que nos olvidemos de tiparlo como A. ¿Por qué?

Porque si hago:

 val mascotas: Pets[Cat] = Pets[Animal](new Animal) 

El compilador se esperaría que mascotas.pet fuese de tipo Cat y pudiese hacer mascotas.pet.meow(), pero mascotas.pet no es Cat, es Animal.

Y pese a que Pets[-A] es contravariante sobre A, el valor pet: A no lo es, una vez definido su tipo(val mascotas: Pets[Cat] implica que mascotas.pet será Cat), este es definitivo.

Si Pets fuese covariante sobre A(Pets[+A]) esto no ocurriría, ya que si hacemos:

val mascotas: Pets[Animal] = Pets[Cat](new Cat)

el compilador esperará que mascotas.pet sea de tipo Animal, y debido a que Cat <: Animal, lo es.


Otro ejemplo:

abstract class Pets[-A] {
 def show(): A
}
 <console>:8: error: contravariant type A occurs in covariant position in type ()A of method show
 def show(): A

Por el mismo motivo de antes, el compilador dice que el tipo de retorno de un método está en posición covariante, A es contravariante.

Si yo hago:

 val mascotas: Pets[Cat] = Pets[Animal](new Animal) 

Esperaría poder hacer mascotas.show.meow(), ya que como mascota es un Pets[Cat], show devolverá un Cat. Pero como ya hemos descubierto antes, en realidad es un Pets[Animal] y show devolverá un Animal.

Finalmente me gustaría mostrar la definición de Function1 (función que acepta un parámetro de entrada) en Scala:

trait Function1[-T, +R] extends AnyRef {
def apply(v1: T): R
}

Al crear una función, ya se nos informa que es del tipo function1:
Si tenemos las clases: class Animal, Class Dog extends Animal, class Bulldog extends Dog y class Cat extends Animal

val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Animal 
 doSomething: Bulldog => Animal = <function1>

Hemos creado una Function1[Bulldog, Animal], pero recordad las varianzas que tiene Function1, R es covariante,
lo que significa que aunque la declaremos como Animal, podemos devolver cualquier subtipo:

val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Cat 
 doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Dog
 doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Bulldog
 doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (bulldoggy) => new Animal
 doSomething: Bulldog => Animal = <function1>

Esto es debido a que por Liskov podemos sustituir Animal por cualquiera de sus subtipos.

Pero T es contravariante, por tanto:

val doSomething:(Bulldog=>Animal) = (doggy: Dog) => new Animal
 doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (animal: Animal) => new Animal
 doSomething: Bulldog => Animal = <function1>
val doSomething:(Bulldog=>Animal) = (kitty: Cat) => new Animal
 /* <console>:11: error: type mismatch;
 found : Cat => Animal
 required: Bulldog => Animal
 val doSomething:(Bulldog=>Animal) = (kitty: Cat) => new Animal
 ^ /*


En la posición de T(Bulldog) podemos tipar el parámetro de entrada como cualquier supertipo de Bulldog,
ya que Bulldog siempre cumplirá con la herencia(Bulldog es Dog, y también es Animal). Vuelve a ser Liskov.


Y esto es todo en cuanto a covarianza, por el momento. En el siguiente artículo, Scala Generics III: The Type constraints nos sumergiremos en el mundo de los type constraints y finalizaremos la introducción a los genéricos!

 

Si este artículo sobre covarianza y contravarianza te gustó, te puede interesar: 

La Deuda Técnica 

Simular respuestas del servidor con Nodejs

Principio de responsabilidad única 

Por qué Kotlin ?

Patrón MVP en iOS

Arquitectura de microservicios  

F-bound en Scala: traits genéricos con higher-kinded types

 

Suscríbete a nuestro newsletter para estar al día de los eventos, meet ups y demás artículos!