Por que o exemplo não compila, também conhecido como (co-, contra- e in) variação funciona?

147

Após essa pergunta , 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 +Te Tna declaração de tipo (ele compila se eu usar T). Mas então como alguém realmente escreve uma classe que é covariante em seu parâmetro de tipo sem recorrer a criar a coisa não parametrizada ? Como posso garantir que o seguinte possa ser criado apenas com uma instância de T?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

EDIT - agora reduzimos para o seguinte:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

isso é tudo de bom, mas agora tenho dois parâmetros de tipo, onde quero apenas um. Vou re-perguntar a pergunta assim:

Como posso escrever uma classe imutável Slot que seja covariante em seu tipo?

EDIT 2 : Duh! Eu usei vare não val. A seguir, é o que eu queria:

class Slot[+T] (val some: T) { 
}
oxbow_lakes
fonte
6
Porque varé configurável enquanto valnão é. É a mesma razão pela qual as coleções imutáveis ​​do scala são covariantes, mas as mutáveis ​​não.
Oxbow_lakes 30/10/09
Isso pode ser interessante nesse contexto: scala-lang.org/old/node/129
user573215 11/11

Respostas:

302

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.

Daniel Spiewak
fonte
4
Isso pode ser declarado em inglês simples como - você pode usar algo mais simples como parâmetro e retornar algo mais complexo?
23412 Phil
1
O compilador Java (1.7.0) não compila "Object [] arr = new int [1];" mas fornece a mensagem de erro: "java: tipos incompatíveis necessários: java.lang.Object [] encontrado: int []". Eu acho que você quis dizer "Object [] arr = new Integer [1];".
Emre Sevinç
2
Quando você mencionou, "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)". Você poderia realmente dar alguns exemplos?
Perryzheng
2
@perryzheng por isso , tome trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)e def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Então, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})tudo bem, pois nosso pastor de animais pode agrupar vacas, mas iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})gera um erro de compilação, pois nosso pastor de vacas não pode agrupar todos os animais.
Lasf
Isso está relacionado e me ajudou com variância: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz
27

@ Daniel explicou muito bem. Mas, para explicar em resumo, se for 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.getem seguida, lançará um erro no tempo de execução, pois não obteve êxito na conversão de um Animalpara Dog(duh!).

Em geral, a mutabilidade não combina bem com co-variação e contra-variação. Essa é a razão pela qual todas as coleções Java são invariantes.

Jatin
fonte
7

Veja Scala por exemplo , página 57+ para uma discussão completa sobre isso.

Se estou entendendo o seu comentário corretamente, você precisa reler a passagem que começa na parte inferior da página 56 (basicamente, o que eu acho que você está pedindo não é seguro para o tipo sem verificações de tempo de execução, o que o scala não faz, então você está sem sorte). Traduzindo o exemplo deles para usar 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ê acha que não estou entendendo sua pergunta (uma possibilidade distinta), tente adicionar mais explicações / contexto à descrição do problema e tentarei novamente.

Em resposta à sua edição: slots imutáveis ​​são uma situação totalmente diferente ... * smile * Espero que o exemplo acima tenha ajudado.

MarkusQ
fonte
Eu li isso; infelizmente eu (ainda) não entendem como eu posso fazer o que eu peço acima (ou seja, na verdade, escrever uma covariante classe parametrizada em T)
oxbow_lakes
Tirei minha marca quando percebi que isso era um pouco duro. Eu deveria ter deixado claro nas perguntas que li os bits de Scala por exemplo; Eu só queria que explicou de forma "menos formal"
oxbow_lakes
@oxbow_lakes smile Eu temo que Scala By Example seja a explicação menos formal. Na melhor das hipóteses, podemos tentar usar exemplos concretos para o trabalho que ele aqui ...
MarkusQ
Desculpe - não quero que meu slot seja mutável. Eu só percebi que o problema é que eu declarou var e não val
oxbow_lakes
3

Você precisa aplicar um limite inferior ao parâmetro. Estou tendo dificuldade em lembrar a sintaxe, mas acho que seria algo como isto:

class Slot[+T, V <: T](var some: V) {
  //blah
}

O Scala-by-example é um pouco difícil de entender, alguns exemplos concretos teriam ajudado.

Saem
fonte