Perché l'esempio non viene compilato, ovvero come funziona la varianza (co-, contra- e in-)?


147

In seguito a questa domanda , qualcuno può spiegare quanto segue in Scala:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

Comprendo la distinzione tra +Te Tnella dichiarazione del tipo (compila se uso T). Ma allora come si può effettivamente scrivere una classe che è covariante nel suo parametro di tipo senza ricorrere alla creazione della cosa non parametrizzata ? Come posso assicurarmi che quanto segue possa essere creato solo con un'istanza di T?

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

MODIFICA : ora riducilo a quanto segue:

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

va tutto bene, ma ora ho due parametri di tipo, dove ne voglio solo uno. Riproverò la domanda in questo modo:

Come posso scrivere una classe immutabile Slot che è covariante nel suo tipo?

EDIT 2 : Duh! Ho usato vare no val. Quanto segue è quello che volevo:

class Slot[+T] (val some: T) { 
}

6
Perché varè impostabile mentre valnon lo è. È lo stesso motivo per cui le collezioni immutabili di Scala sono covarianti ma non quelle mutabili.
oxbow_lakes,

Questo potrebbe essere interessante in questo contesto: scala-lang.org/old/node/129
user573215

Risposte:


302

Generalmente, un parametro di tipo covariante è uno che può variare in base alla sottotipo della classe (in alternativa, variare con il sottotipo, da cui il prefisso "co-"). Più concretamente:

trait List[+A]

List[Int]è un sottotipo di List[AnyVal]poiché Intè un sottotipo di AnyVal. Ciò significa che è possibile fornire un'istanza di List[Int]quando List[AnyVal]è previsto un valore di tipo . Questo è davvero un modo molto intuitivo per far funzionare i generici, ma si scopre che è insensato (rompe il sistema dei tipi) se usato in presenza di dati mutabili. Questo è il motivo per cui i generici sono invarianti in Java. Breve esempio di instabilità nell'uso di array Java (che sono erroneamente covarianti):

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

Abbiamo appena assegnato un valore di tipo Stringa un array di tipo Integer[]. Per ragioni che dovrebbero essere ovvie, questa è una cattiva notizia. Il sistema di tipi Java in realtà lo consente al momento della compilazione. La JVM lancerà "utile" ArrayStoreExceptionin fase di esecuzione. Il sistema di tipi di Scala evita questo problema perché il parametro type sulla Arrayclasse è invariante (dichiarazione [A]invece che [+A]).

Si noti che esiste un altro tipo di varianza noto come contravarianza . Questo è molto importante perché spiega perché la covarianza può causare alcuni problemi. La contraddizione è letteralmente l'opposto della covarianza: i parametri variano verso l'alto con il sottotipo. È molto meno comune in parte perché è così controintuitivo, sebbene abbia un'applicazione molto importante: le funzioni.

trait Function1[-P, +R] {
  def apply(p: P): R
}

Notare l' annotazione della varianza " - " sul Pparametro type. Questa dichiarazione nel suo insieme significa che Function1è contraddittoria Pe covariante in R. Pertanto, possiamo derivare i seguenti assiomi:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

Si noti che T1'deve essere un sottotipo (o lo stesso tipo) di T1, mentre è l'opposto per T2e T2'. In inglese, questo può essere letto come segue:

Una funzione A è un sottotipo di un'altra funzione B se il tipo di parametro di A è un supertipo del tipo di parametro di B , mentre il tipo di ritorno di A è un sottotipo del tipo restituito di B .

Il motivo di questa regola è lasciato al lettore come esercizio (suggerimento: pensa a casi diversi in quanto le funzioni sono sottotitolate, come il mio esempio di matrice dall'alto).

Con la tua nuova conoscenza della co-e contravarianza, dovresti essere in grado di capire perché il seguente esempio non verrà compilato:

trait List[+A] {
  def cons(hd: A): List[A]
}

Il problema è che Aè covariante, mentre la consfunzione prevede che il suo parametro di tipo sia invariante . Quindi, Asta variando la direzione sbagliata. È interessante notare che potremmo risolvere questo problema rendendoci Listcontroversi A, ma il tipo restituito non List[A]sarebbe valido poiché la consfunzione prevede che il suo tipo restituito sia covariante .

Le nostre uniche due opzioni qui sono: a) rendere Ainvarianti, perdendo le proprietà di digitazione di covarianza belle e intuitive, oppure b) aggiungere un parametro di tipo locale al consmetodo che definisce Aun limite inferiore:

def cons[B >: A](v: B): List[B]

Questo è ora valido. Potete immaginare che Astia cambiando verso il basso, ma Bè in grado di variare verso l'alto rispetto a Apoiché Aè il suo limite inferiore. Con questa dichiarazione del metodo, possiamo Aessere covarianti e tutto funziona.

Nota che questo trucco funziona solo se restituiamo un'istanza di Listcui è specializzato il tipo meno specifico B. Se si tenta di rendere Listmutevole, le cose si rompono poiché si finisce per provare ad assegnare valori di tipo Ba una variabile di tipo A, che non è consentita dal compilatore. Ogni volta che si ha una mutabilità, è necessario disporre di un mutatore di qualche tipo, che richiede un parametro di metodo di un certo tipo, che (insieme all'accessorio) implica invarianza. Covariance funziona con dati immutabili poiché l'unica operazione possibile è un accessor, a cui può essere assegnato un tipo di ritorno covariante.


4
Questo potrebbe essere dichiarato in un inglese semplice come: puoi prendere qualcosa di più semplice come parametro e puoi restituire qualcosa di più complesso?
Phil

1
Il compilatore Java (1.7.0) non compila "Object [] arr = new int [1];" ma fornisce piuttosto il messaggio di errore: "java: tipi incompatibili richiesti: java.lang.Object [] found: int []". Penso che volevi dire "Object [] arr = new Integer [1];".
Emre Sevinç,

2
Quando hai accennato, "Il motivo di questa regola è lasciato al lettore come esercizio (suggerimento: pensa a casi diversi in quanto le funzioni sono sottotitolate, come il mio esempio di matrice dall'alto)." Potresti davvero dare un paio di esempi?
Perryzheng,

2
@perryzheng per questo , prendere 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). Quindi, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})va bene, poiché il nostro allevatore di animali può mandare mucche, ma iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})dà un errore di compilazione, poiché il nostro allevatore di mucche non può mandare tutti gli animali.
Lasf,

Questo è collegato e mi ha aiutato con la varianza: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz,

27

@Daniel l'ha spiegato molto bene. Ma per spiegarlo in breve, se fosse permesso:

  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.getgenererà quindi un errore in fase di esecuzione poiché non è riuscito a convertire un Animalin Dog(duh!).

In generale, la mutabilità non va bene con la co-varianza e la contro-varianza. Questo è il motivo per cui tutte le raccolte Java sono invarianti.


7

Vedi Scala per esempio , pagina 57+ per una discussione completa di questo.

Se capisco correttamente il tuo commento, devi rileggere il passaggio a partire dalla fine di pagina 56 (in pratica, ciò che penso che stai chiedendo non è sicuro dal punto di vista del tipo senza controlli del tempo di esecuzione, cosa che scala non fa, quindi sei sfortunato). Traducendo il loro esempio per usare il tuo costrutto:

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 ritieni che non capisca la tua domanda (una possibilità distinta), prova ad aggiungere ulteriori spiegazioni / contesto alla descrizione del problema e riproverò.

In risposta alla tua modifica: le slot immutabili sono una situazione completamente diversa ... * sorridi * Spero che l'esempio sopra sia stato di aiuto.


L'ho letto; sfortunatamente (ancora) non capisco come posso fare quello che chiedo sopra (ovvero scrivere effettivamente una covariante di classe parametrizzata in T)
oxbow_lakes

Ho rimosso il mio downmark quando mi sono reso conto che era un po 'duro. Avrei dovuto chiarire nelle domande che avevo letto i brani di Scala con l'esempio; Volevo solo spiegarlo in modo "meno formale"
oxbow_lakes,

@oxbow_lakes smile Temo che Scala By Example sia la spiegazione meno formale. Nella migliore delle ipotesi, possiamo provare a usare esempi concreti per lavorare qui ...
MarkusQ

Spiacente, non voglio che il mio slot sia mutabile. Mi sono appena reso conto che il problema è che ho dichiarato var e non val
oxbow_lakes il

3

È necessario applicare un limite inferiore al parametro. Sto facendo fatica a ricordare la sintassi, ma penso che sarebbe simile a questo:

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

La Scala per esempio è un po 'difficile da capire, alcuni esempi concreti avrebbero aiutato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.