Genéricamente, un parámetro de tipo covariante es aquel que puede variar a medida que la clase se subtipea (alternativamente, varía según el subtipo, de ahí el prefijo "co-"). Más concretamente:
trait List[+A]
List[Int]
es un subtipo de List[AnyVal]
porque Int
es un subtipo de AnyVal
. Esto significa que puede proporcionar una instancia de List[Int]
cuándo List[AnyVal]
se espera un valor de tipo . Esta es realmente una forma muy intuitiva para que funcionen los genéricos, pero resulta que no es sólida (rompe el sistema de tipos) cuando se usa en presencia de datos mutables. Es por eso que los genéricos son invariantes en Java. Breve ejemplo de falta de solidez utilizando matrices Java (que son erróneamente covariantes):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Acabamos de asignar un valor de tipo String
a una matriz de tipo Integer[]
. Por razones que deberían ser obvias, estas son malas noticias. El sistema de tipos de Java realmente permite esto en tiempo de compilación. La JVM lanzará un "útilmente" ArrayStoreException
en tiempo de ejecución. El sistema de tipos de Scala previene este problema porque el parámetro de tipo en la Array
clase es invariable (la declaración es [A]
más que [+A]
).
Tenga en cuenta que hay otro tipo de varianza conocida como contravarianza . Esto es muy importante ya que explica por qué la covarianza puede causar algunos problemas. La contravarianza es literalmente lo opuesto a la covarianza: los parámetros varían hacia arriba con el subtipo. Es mucho menos común en parte porque es muy intuitivo, aunque tiene una aplicación muy importante: las funciones.
trait Function1[-P, +R] {
def apply(p: P): R
}
Observe la anotación de varianza " - " en el P
parámetro de tipo. Esta declaración en su conjunto significa que Function1
es contravariante P
y covariante en R
. Por lo tanto, podemos derivar los siguientes axiomas:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Tenga en cuenta que T1'
debe ser un subtipo (o el mismo tipo) de T1
, mientras que es lo contrario para T2
y T2'
. En inglés, esto se puede leer de la siguiente manera:
Una función A es un subtipo de otra función B si el tipo de parámetro de A es un supertipo del tipo de parámetro de B mientras que el tipo de retorno de A es un subtipo del tipo de retorno de B .
La razón de esta regla se deja como un ejercicio para el lector (pista: piense en diferentes casos a medida que las funciones están subtipadas, como mi ejemplo de matriz de arriba).
Con su nuevo conocimiento de co y contravarianza, debería poder ver por qué el siguiente ejemplo no se compilará:
trait List[+A] {
def cons(hd: A): List[A]
}
El problema es que A
es covariante, mientras que la cons
función espera que su parámetro de tipo sea invariante . Por lo tanto, A
está variando la dirección equivocada. Curiosamente, podríamos resolver este problema haciendo List
contravariante A
, pero luego el tipo de retorno List[A]
sería inválido ya que la cons
función espera que su tipo de retorno sea covariante .
Nuestras únicas dos opciones aquí son: a) hacer A
invariante, perder las propiedades agradables e intuitivas de subtipo de covarianza, o b) agregar un parámetro de tipo local al cons
método que se define A
como un límite inferior:
def cons[B >: A](v: B): List[B]
Esto ahora es válido. Se puede imaginar que A
está variando hacia abajo, pero B
es capaz de variar hacia arriba con respecto a A
ya A
es su límite inferior. Con esta declaración de método, podemos A
ser covariantes y todo funciona.
Tenga en cuenta que este truco solo funciona si devolvemos una instancia List
especializada en el tipo menos específico B
. Si intenta hacer List
mutable, las cosas se descomponen ya que termina tratando de asignar valores de tipo B
a una variable de tipo A
, que el compilador no permite. Siempre que tenga mutabilidad, debe tener un mutador de algún tipo, que requiere un parámetro de método de cierto tipo, que (junto con el descriptor de acceso) implica invariancia. La covarianza funciona con datos inmutables ya que la única operación posible es un descriptor de acceso, al que se le puede dar un tipo de retorno covariante.
var
es configurable mientrasval
que no lo es. Es la misma razón por la cual las colecciones inmutables de scala son covariantes, pero las mutables no lo son.