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

Seguendo questa domanda , qualcuno può spiegare quanto segue in Scala:

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

Capisco la distinzione tra +T e T nella dichiarazione del tipo (compila se uso T ). Ma allora come si scrive effettivamente una class che è covariante nel suo parametro di tipo senza ricorrere alla creazione della cosa non parametrizzata ? Come posso garantire che quanto segue possa essere creato solo con un’istanza di T ?

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

EDIT – ora ottenuto questo al seguente:

 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 uno solo. Riprenderò la domanda così:

Come posso scrivere una class di Slot immutabile che è covariante nel suo genere?

EDIT 2 : Duh! Ho usato var e non val . Quello che segue è quello che volevo:

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

Generalmente, un parametro di tipo covariante è uno che può variare in quanto la class è sottotipizzata (in alternativa, varia con sottotipizzazione, da cui il prefisso “co-“). Più concretamente:

 trait List[+A] 

List[Int] è un sottotipo di List[AnyVal] perché Int è un sottotipo di AnyVal . Ciò significa che è ansible fornire un’istanza di List[Int] quando è previsto un valore di tipo List[AnyVal] . Questo è davvero un modo molto intuitivo per i generici di funzionare, ma risulta che non è corretto (rompe il sistema di tipi) quando viene utilizzato in presenza di dati mutabili. Questo è il motivo per cui i generici sono invarianti in Java. Breve esempio di insicurezza che utilizza matrici Java (che sono erroneamente covarianti):

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

Abbiamo appena assegnato un valore di tipo String a un array di tipo Integer[] . Per ragioni che dovrebbero essere ovvie, questa è una brutta notizia. Il sistema di tipo Java in realtà lo consente al momento della compilazione. La JVM “generosamente” lancia un ArrayStoreException in fase di runtime. Il sistema tipo Scala impedisce questo problema perché il parametro type nella class Array è invariante (la dichiarazione è [A] piuttosto che [+A] ).

Si noti che esiste un altro tipo di varianza noto come controvarianza . Questo è molto importante in quanto spiega perché la covarianza può causare alcuni problemi. La controvarianza è letteralmente l’opposto della covarianza: i parametri variano verso l’alto con sottotipizzazione. È molto meno comune in parte perché è così contro-intuitivo, anche se ha un’applicazione molto importante: le funzioni.

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

Si noti l’annotazione della varianza ” ” sul parametro di tipo P Questa dichiarazione nel suo insieme significa che Function1 è controvariante in P e covariante in R Quindi, possiamo ricavare 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 T2 e 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 di ritorno di B.

La ragione di questa regola è lasciata come esercizio al lettore (suggerimento: si pensi a casi diversi in quanto le funzioni sono sottotipizzate, come il mio esempio di array dall'alto).

Con la tua nuova conoscenza della co- e controvarianza, 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 funzione cons aspetta che il suo parametro type sia invariante . Quindi, A sta variando la direzione sbagliata. È interessante notare che potremmo risolvere questo problema rendendo List controvariante in A , ma il tipo di ritorno List[A] sarebbe valido poiché la funzione cons aspetta che il suo tipo di ritorno sia covariante .

Le nostre uniche due opzioni sono: a) creare A invariante, perdendo le proprietà di covarianza piacevoli e intuitive della covarianza, oppure b) aggiungere un parametro di tipo locale al metodo cons che definisce A come limite inferiore:

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

Questo è ora valido. Potete immaginare che A sia variabile verso il basso, ma B è in grado di variare verso l'alto rispetto ad A poiché A è il limite inferiore. Con questa dichiarazione di metodo, possiamo avere A essere covariante e tutto funziona.

Si noti che questo trucco funziona solo se restituiamo un'istanza di List specializzata nel tipo B meno specifico. Se si tenta di rendere la List mutabile, le cose si guastano da quando si tenta di assegnare valori di tipo B a una variabile di tipo A , che non è consentita dal compilatore. Ogni volta che si ha la mutabilità, è necessario avere un mutatore di qualche tipo, che richiede un parametro di metodo di un certo tipo, che (insieme all'accessorio) implica l'invarianza. La covarianza funziona con dati immutabili poiché l'unica operazione ansible è un accessor, a cui può essere assegnato un tipo di ritorno covariante.

@ 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.get emetterà quindi un errore in fase di esecuzione poiché non ha avuto successo nella conversione di un Animal in un 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.

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

Se sto capendo correttamente il tuo commento, devi rileggere il passaggio iniziando dal fondo della pagina 56 (in sostanza, quello che penso tu stia chiedendo non è sicuro dal punto di vista del tipo senza verifiche del tempo di esecuzione, 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 di non capire la tua domanda (una possibilità distinta), prova ad aggiungere più spiegazione / contesto alla descrizione del problema e ci riproverò.

In risposta alla tua modifica: Gli slot immutabili sono una situazione completamente diversa … * smile * Spero che l’esempio sopra sia stato d’aiuto.

È necessario applicare un limite inferiore al parametro. Sto avendo difficoltà a ricordare la syntax, ma penso che sarebbe simile a questo:

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

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