Por que não a exemplo de compilação, aka como é (co-, e in- contra-) trabalho variância?
-
20-08-2019 - |
Pergunta
Na sequência esta questão , alguém pode explicar o seguinte em Scala:
class Slot[+T] (var some: T) {
// DOES NOT COMPILE
// "COVARIANT parameter in CONTRAVARIANT position"
}
Eu entendo a distinção entre +T
e T
na declaração do tipo (ele compila se eu usar T
). Mas então como é que uma pessoa realmente escrever uma classe que é covariante em seu parâmetro de tipo sem recorrer a criar a coisa unparametrized ? Como posso garantir que o seguinte só pode ser criado com uma instância de T
?
class Slot[+T] (var some: Object){
def get() = { some.asInstanceOf[T] }
}
Editar - agora tenho esse baixo para o seguinte:
abstract class _Slot[+T, V <: T] (var some: V) {
def getT() = { some }
}
tudo isso é bom, mas agora tenho dois parâmetros de tipo, onde eu só quero um. Eu vou voltar a fazer a pergunta assim:
Como posso escrever um imutável classe Slot
que é covariant em seu tipo?
EDIT 2 : Duh! Eu costumava var
e não val
. O seguinte é o que eu queria:
class Slot[+T] (val some: T) {
}
Solução
Genericamente, um covariante parâmetro tipo é um que é permitido variar para baixo como a classe é subtipadas (alternativamente, variar com subtipagem, portanto, 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 é esperado um valor do tipo List[AnyVal]
. Esta é realmente uma maneira muito intuitiva para os genéricos para trabalhar, mas verifica-se que ele não é sólido (quebras o sistema de tipo), quando utilizado na presença de dados mutáveis. É por isso que os genéricos são invariantes em Java. Breve exemplo de inconsistência utilizando matrizes de Java (que são erroneamente covariante):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Nós apenas atribuído um valor de tipo String
para uma matriz do tipo Integer[]
. Por razões que devem ser óbvias, esta é uma má notícia. sistema de tipo de Java realmente permite esta em tempo de compilação. O JVM vai "amavelmente" lançar uma ArrayStoreException
em tempo de execução. sistema tipo impede de Scala esse problema porque o parâmetro de tipo na classe Array
é invariante (declaração é [A]
em vez de [+A]
).
Note que existe um outro tipo de variância conhecida como contravariance . Isto é muito importante, pois explica porque covariância pode causar alguns problemas. Contravariance é literalmente o oposto de covariância: parâmetros variam para cima com subtipagem. É muito menos comum parcialmente porque é tão contra-intuitivo, embora ele não tem muito importante aplicação:. Funções
trait Function1[-P, +R] {
def apply(p: P): R
}
Observe o " - " anotação variação no parâmetro de tipo P
. Esta declaração como um todo significa que Function1
é contravariant em P
e covariante em R
. Assim, podemos obter 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
, que é o oposto para T2
e T2'
. Em Inglês, isso pode ser lido como o seguinte:
Função A A é um subtipo de outra função B , se o tipo de parâmetro de A é um supertipo do tipo de parâmetro de B , enquanto o tipo de retorno de a é um subtipo do tipo de retorno de B .
A razão para esta regra é deixada como um exercício para o leitor. (Dica: pense em casos diferentes como funções são subtipadas, como meu exemplo matriz de acima)
Com o seu conhecimento recém-descoberto de co- e contravariance, você deve ser capaz de ver por exemplo a seguir não será compilado:
trait List[+A] {
def cons(hd: A): List[A]
}
O problema é que A
é covariant, enquanto a função cons
espera que seu parâmetro de tipo a ser invariante . Assim, A
está variando a direção errada. Curiosamente, podemos resolver este problema, tornando List
contravariant em A
, mas, em seguida, o tipo de retorno List[A]
seria inválida como a função cons
espera seu tipo de retorno para ser covariant .
Nossos apenas duas opções aqui estão a um) make A
invariante, perdendo a agradável, propriedades intuitiva sub-digitação de covariância, ou b) adicionar um parâmetro de tipo local para o método cons
que define A
como um limite inferior:
def cons[B >: A](v: B): List[B]
Esta é agora válido. Você pode imaginar que A
está variando para baixo, mas B
é capaz de variar para cima em relação ao A
desde A
é o seu limite inferior. Com esta declaração de método, podemos ter A
ser covariantes e tudo funciona.
Observe que this truque só funciona se retornar uma instância de List
que é especializada no tipo B
menos específica. Se você tentar fazer List
mutável, as coisas quebrar desde que você acabar tentando valores Atribuir do tipo B
a uma variável do tipo A
, o que não é permitido pelo compilador. Sempre que você tem mutabilidade, você precisa ter um modificador de algum tipo, o que requer um parâmetro de método de um certo tipo, que (juntamente com o acessor) implica invariância. Covariance trabalha com dados imutáveis ??como a única possível operação é um assessor, que pode ser dada um tipo de retorno covariant.
Outras dicas
@ Daniel explicou isso muito bem. Mas, para explicar em suma, se fosse permitido:
class Slot[+T](var some: T) {
def get: T = some
}
val slot: Slot[Dog] = new Slot[Dog](new Dog)
val slot2: Slot[Animal] = slot //because of co-variance
slot2.some = new Animal //legal as some is a var
slot.get ??
slot.get
, então, lançar um erro em tempo de execução, uma vez que não teve sucesso em converter um Animal
para Dog
(duh!).
Em mutabilidade geral não vão bem com co-variância e contra-variância. Essa é a razão pela qual todas as coleções de Java são invariáveis.
Scala por exemplo , página 57+ para uma discussão completa deste.
Se eu estou entendendo o seu comentário corretamente, você precisa reler a passagem começando na parte inferior da página 56 (basicamente, o que eu acho que você está pedindo não é tipo seguro sem verificações de tempo de execução, que scala doesn' t fazer, então você está fora de sorte). Traduzindo seu exemplo de usar a sua construção:
val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2)) // Works, but now x.get() will blow up
Se você sentir que não estou entendendo sua pergunta (uma possibilidade distinta), tente adicionar mais explicações / contexto para a descrição do problema e eu vou tentar novamente.
Em resposta à sua edição: ranhuras imutáveis ??são uma situação totalmente diferente ... * sorriso * Espero que o exemplo acima ajudou
.Você precisa aplicar um limite sobre o parâmetro mais baixo. Estou tendo dificuldade em lembrar a sintaxe, mas eu acho que seria algo parecido com isto:
class Slot[+T, V <: T](var some: V) {
//blah
}
O Scala-by-exemplo é um pouco difícil de entender, alguns exemplos concretos teria ajudado.