Genericamente, um parâmetro do tipo covariante é aquele que pode variar conforme a classe é subtipada (alternativamente, varia com a subtipagem, daí o prefixo "co-"). Mais concretamente:
trait List[+A]
List[Int]é um subtipo de List[AnyVal]porque Inté um subtipo de AnyVal. Isso significa que você pode fornecer uma instância de List[Int]quando um valor do tipo List[AnyVal]é esperado. Essa é realmente uma maneira muito intuitiva para os genéricos funcionarem, mas acontece que é incorreto (quebra o sistema de tipos) quando usado na presença de dados mutáveis. É por isso que os genéricos são invariantes em Java. Breve exemplo de insatisfação usando matrizes Java (erroneamente covariantes):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Acabamos de atribuir um valor do tipo Stringa uma matriz do tipo Integer[]. Por razões que deveriam ser óbvias, são más notícias. O sistema de tipos Java realmente permite isso em tempo de compilação. A JVM lançará um "útil" um ArrayStoreExceptionem tempo de execução. O sistema de tipos do Scala evita esse problema porque o parâmetro de tipo na Arrayclasse é invariável (a declaração é [A]melhor que [+A]).
Observe que há outro tipo de variação conhecido como contravariância . Isso é muito importante, pois explica por que a covariância pode causar alguns problemas. Contravariância é literalmente o oposto de covariância: os parâmetros variam para cima com a subtipagem. É muito menos comum parcialmente porque é muito contra-intuitivo, embora tenha uma aplicação muito importante: funções.
trait Function1[-P, +R] {
def apply(p: P): R
}
Observe a anotação de variação " - " no Pparâmetro type. Esta declaração como um todo significa que Function1é contravariante Pe covariante em R. Assim, podemos derivar os seguintes axiomas:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Observe que T1'deve ser um subtipo (ou o mesmo tipo) de T1, enquanto que é o oposto de T2e T2'. Em inglês, isso pode ser lido da seguinte maneira:
Uma função Um é um subtipo de uma outra função B , se o tipo de parâmetro de Um é um supertipo do tipo de parâmetro de B , enquanto o tipo de retorno Um é um subtipo do tipo de retorno de B .
O motivo desta regra é deixado como um exercício para o leitor (dica: pense em casos diferentes, pois as funções são subtipadas, como meu exemplo de matriz acima).
Com seu conhecimento recém-encontrado de co- e contravariância, você poderá ver por que o exemplo a seguir não será compilado:
trait List[+A] {
def cons(hd: A): List[A]
}
O problema é que Aé covariante, enquanto a consfunção espera que seu parâmetro de tipo seja invariável . Assim, Aestá variando na direção errada. Curiosamente, poderíamos resolver esse problema tornando Listcontravariante A, mas o tipo de retorno List[A]seria inválido, pois a consfunção espera que seu tipo de retorno seja covariante .
Nossas únicas duas opções aqui são: a) Ainvariável, perdendo as boas e intuitivas propriedades de sub-digitação da covariância, ou b) adicionar um parâmetro de tipo local ao consmétodo que define Acomo um limite inferior:
def cons[B >: A](v: B): List[B]
Isso agora é válido. Você pode imaginar que Aestá variando para baixo, mas Bé capaz de variar para cima em relação a Auma vez que Aé seu limite inferior. Com esta declaração de método, podemos Aser covariantes e tudo dá certo.
Observe que esse truque só funciona se retornarmos uma instância Listespecializada no tipo menos específico B. Se você tentar tornar Listmutável, as coisas se deterioram, pois você acaba tentando atribuir valores do tipo Ba uma variável do tipo A, o que não é permitido pelo compilador. Sempre que você tem mutabilidade, é necessário ter algum tipo de mutador, o que requer um parâmetro de método de um determinado tipo, o qual (junto com o acessador) implica invariância. A covariância trabalha com dados imutáveis, pois a única operação possível é um acessador, que pode receber um tipo de retorno covariante.
varé configurável enquantovalnão é. É a mesma razão pela qual as coleções imutáveis do scala são covariantes, mas as mutáveis não.