Secara umum, parameter tipe kovarian adalah salah satu yang diizinkan untuk bervariasi ke bawah saat kelas subtipe (atau, berbeda dengan subtipe, maka awalan "co-"). Lebih konkret:
trait List[+A]
List[Int]
adalah subtipe List[AnyVal]
karena Int
merupakan subtipe dari AnyVal
. Ini berarti bahwa Anda dapat memberikan contoh List[Int]
kapan nilai tipe List[AnyVal]
diharapkan. Ini benar-benar cara yang sangat intuitif untuk obat generik untuk bekerja, tetapi ternyata tidak sehat (memecah sistem tipe) saat digunakan di hadapan data yang dapat diubah. Inilah sebabnya mengapa obat generik tidak tetap di Jawa. Contoh singkat ketidaknyamanan menggunakan array Java (yang keliru kovarian):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Kami baru saja menetapkan nilai tipe String
ke array tipe Integer[]
. Untuk alasan yang harus jelas, ini adalah berita buruk. Sistem tipe Java sebenarnya memungkinkan ini pada waktu kompilasi. JVM akan "membantu" melempar ArrayStoreException
pada saat runtime. Sistem tipe Scala mencegah masalah ini karena parameter tipe pada Array
kelas adalah invarian (deklarasi [A]
bukan [+A]
).
Perhatikan bahwa ada jenis varians lain yang dikenal sebagai contravariance . Ini sangat penting karena menjelaskan mengapa kovarians dapat menyebabkan beberapa masalah. Kontravarians secara harfiah berlawanan dengan kovarians: parameter bervariasi ke atas dengan subtyping. Ini jauh kurang umum sebagian karena sangat kontra-intuitif, meskipun memang memiliki satu aplikasi yang sangat penting: fungsi.
trait Function1[-P, +R] {
def apply(p: P): R
}
Perhatikan anotasi varian " - " pada P
parameter tipe. Deklarasi ini secara keseluruhan berarti Function1
bersifat contravariant in P
dan covariant in R
. Dengan demikian, kita dapat menurunkan aksioma berikut:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Perhatikan bahwa T1'
harus berupa subtipe (atau jenis yang sama) dari T1
, sedangkan itu adalah kebalikan dari T2
dan T2'
. Dalam bahasa Inggris, ini dapat dibaca sebagai berikut:
Fungsi A adalah subtipe dari fungsi lain B jika jenis parameter A adalah supertype dari jenis parameter dari B sedangkan tipe kembalinya A adalah subtipe dari jenis kembalinya B .
Alasan aturan ini dibiarkan sebagai latihan bagi pembaca (petunjuk: pikirkan tentang kasus yang berbeda karena fungsi subtipe, seperti contoh array saya dari atas).
Dengan pengetahuan co-dan contravariance yang baru Anda temukan, Anda seharusnya dapat melihat mengapa contoh berikut tidak dapat dikompilasi:
trait List[+A] {
def cons(hd: A): List[A]
}
Masalahnya adalah itu A
kovarian, sementara cons
fungsi mengharapkan parameter tipenya menjadi invarian . Dengan demikian, A
memvariasikan arah yang salah. Yang cukup menarik, kita bisa menyelesaikan masalah ini dengan membuat List
contravarian A
, tetapi kemudian tipe pengembalian List[A]
tidak valid karena cons
fungsi mengharapkan tipe pengembaliannya menjadi kovarian .
Hanya dua opsi kami di sini adalah a) membuat A
invarian, kehilangan sifat sub-pengetikan yang bagus dan intuitif kovarians, atau b) menambahkan parameter tipe lokal ke cons
metode yang didefinisikan A
sebagai batas bawah:
def cons[B >: A](v: B): List[B]
Ini sekarang valid. Anda dapat membayangkan bahwa A
itu bervariasi ke bawah, tetapi B
dapat bervariasi ke atas sehubungan dengan A
itu A
adalah batas bawahnya. Dengan deklarasi metode ini, kita bisa A
menjadi kovarian dan semuanya berjalan lancar.
Perhatikan bahwa trik ini hanya berfungsi jika kita mengembalikan instance List
yang dikhususkan untuk tipe yang kurang spesifik B
. Jika Anda mencoba membuat List
bisa berubah, hal-hal rusak karena Anda akhirnya mencoba untuk menetapkan nilai tipe B
ke variabel tipe A
, yang tidak diizinkan oleh kompiler. Setiap kali Anda memiliki mutabilitas, Anda perlu memiliki semacam mutator, yang memerlukan parameter metode tipe tertentu, yang (bersama-sama dengan accessor) menyiratkan invarian. Kovarian bekerja dengan data yang tidak dapat diubah karena satu-satunya operasi yang mungkin adalah accessor, yang dapat diberikan tipe pengembalian kovarian.
var
bisa diselesaikan sementaraval
tidak. Itu adalah alasan yang sama mengapa koleksi abadi scala adalah kovarian tetapi yang tidak bisa berubah.