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 String
a 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" ArrayStoreException
in fase di esecuzione. Il sistema di tipi di Scala evita questo problema perché il parametro type sulla Array
classe è 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 P
parametro type. Questa dichiarazione nel suo insieme significa che Function1
è contraddittoria P
e 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 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 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 cons
funzione prevede che il suo parametro di tipo sia invariante . Quindi, A
sta variando la direzione sbagliata. È interessante notare che potremmo risolvere questo problema rendendoci List
controversi A
, ma il tipo restituito non List[A]
sarebbe valido poiché la cons
funzione prevede che il suo tipo restituito sia covariante .
Le nostre uniche due opzioni qui sono: a) rendere A
invarianti, perdendo le proprietà di digitazione di covarianza belle e intuitive, oppure b) aggiungere un parametro di tipo locale al cons
metodo che definisce A
un limite inferiore:
def cons[B >: A](v: B): List[B]
Questo è ora valido. Potete immaginare che A
stia cambiando verso il basso, ma B
è in grado di variare verso l'alto rispetto a A
poiché A
è il suo limite inferiore. Con questa dichiarazione del metodo, possiamo A
essere covarianti e tutto funziona.
Nota che questo trucco funziona solo se restituiamo un'istanza di List
cui è specializzato il tipo meno specifico B
. Se si tenta di rendere List
mutevole, le cose si rompono poiché si finisce per provare ad assegnare valori di tipo B
a 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.
var
è impostabile mentreval
non lo è. È lo stesso motivo per cui le collezioni immutabili di Scala sono covarianti ma non quelle mutabili.