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 String
a 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 ArrayStoreException
em tempo de execução. O sistema de tipos do Scala evita esse problema porque o parâmetro de tipo na Array
classe é 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 P
parâmetro type. Esta declaração como um todo significa que Function1
é contravariante P
e 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 T2
e 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 cons
função espera que seu parâmetro de tipo seja invariável . Assim, A
está variando na direção errada. Curiosamente, poderíamos resolver esse problema tornando List
contravariante A
, mas o tipo de retorno List[A]
seria inválido, pois a cons
função espera que seu tipo de retorno seja covariante .
Nossas únicas duas opções aqui são: a) A
invariável, perdendo as boas e intuitivas propriedades de sub-digitação da covariância, ou b) adicionar um parâmetro de tipo local ao cons
método que define A
como um limite inferior:
def cons[B >: A](v: B): List[B]
Isso agora é válido. Você pode imaginar que A
está variando para baixo, mas B
é capaz de variar para cima em relação a A
uma vez que A
é seu limite inferior. Com esta declaração de método, podemos A
ser covariantes e tudo dá certo.
Observe que esse truque só funciona se retornarmos uma instância List
especializada no tipo menos específico B
. Se você tentar tornar List
mutável, as coisas se deterioram, pois você acaba tentando atribuir valores do tipo B
a 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 enquantoval
não é. É a mesma razão pela qual as coleções imutáveis do scala são covariantes, mas as mutáveis não.