Mengapa contoh tidak mengkompilasi, alias bagaimana varians (co, kontra, dan in-) bekerja?


147

Sebagai lanjutan dari pertanyaan ini , dapatkah seseorang menjelaskan yang berikut di Scala:

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

}

Saya memahami perbedaan antara +Tdan Tdalam deklarasi tipe (ini mengkompilasi jika saya menggunakan T). Tetapi kemudian bagaimana seseorang benar-benar menulis kelas yang kovarian dalam parameter tipenya tanpa terpaksa menciptakan hal yang tidak diparameterisasi ? Bagaimana saya bisa memastikan bahwa yang berikut ini hanya dapat dibuat dengan instance dari T?

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

EDIT - sekarang dapatkan ini sebagai berikut:

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

ini semua baik, tapi saya sekarang memiliki dua tipe parameter, di mana saya hanya ingin satu. Saya akan kembali mengajukan pertanyaan sebagai berikut:

Bagaimana saya bisa menulis kelas yang tidak berubah Slot yang kovarian dalam tipenya?

EDIT 2 : Duh! Saya menggunakan vardan tidak val. Berikut ini yang saya inginkan:

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

6
Karena varbisa diselesaikan sementara valtidak. Itu adalah alasan yang sama mengapa koleksi abadi scala adalah kovarian tetapi yang tidak bisa berubah.
oxbow_lakes

Ini mungkin menarik dalam konteks ini: scala-lang.org/old/node/129
user573215

Jawaban:


302

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 Intmerupakan 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 Stringke 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 ArrayStoreExceptionpada saat runtime. Sistem tipe Scala mencegah masalah ini karena parameter tipe pada Arraykelas 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 Pparameter tipe. Deklarasi ini secara keseluruhan berarti Function1bersifat contravariant in Pdan 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 T2dan 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 Akovarian, sementara consfungsi mengharapkan parameter tipenya menjadi invarian . Dengan demikian, Amemvariasikan arah yang salah. Yang cukup menarik, kita bisa menyelesaikan masalah ini dengan membuat Listcontravarian A, tetapi kemudian tipe pengembalian List[A]tidak valid karena consfungsi mengharapkan tipe pengembaliannya menjadi kovarian .

Hanya dua opsi kami di sini adalah a) membuat Ainvarian, kehilangan sifat sub-pengetikan yang bagus dan intuitif kovarians, atau b) menambahkan parameter tipe lokal ke consmetode yang didefinisikan Asebagai batas bawah:

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

Ini sekarang valid. Anda dapat membayangkan bahwa Aitu bervariasi ke bawah, tetapi Bdapat bervariasi ke atas sehubungan dengan Aitu Aadalah batas bawahnya. Dengan deklarasi metode ini, kita bisa Amenjadi kovarian dan semuanya berjalan lancar.

Perhatikan bahwa trik ini hanya berfungsi jika kita mengembalikan instance Listyang dikhususkan untuk tipe yang kurang spesifik B. Jika Anda mencoba membuat Listbisa berubah, hal-hal rusak karena Anda akhirnya mencoba untuk menetapkan nilai tipe Bke 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.


4
Mungkinkah ini dinyatakan dalam bahasa Inggris sebagai - Anda dapat mengambil sesuatu yang lebih sederhana sebagai parameter dan Anda dapat mengembalikan sesuatu yang lebih kompleks?
Phil

1
Kompiler Java (1.7.0) tidak mengkompilasi "Object [] arr = new int [1];" melainkan memberikan pesan kesalahan: "java: diperlukan jenis yang tidak kompatibel: java.lang.Object [] ditemukan: int []". Saya pikir Anda maksud "Object [] arr = new Integer [1];".
Emre Sevinç

2
Ketika Anda menyebutkan, "Alasan untuk aturan ini dibiarkan sebagai latihan untuk pembaca (petunjuk: pikirkan tentang kasus yang berbeda karena fungsi subtipe, seperti contoh array saya dari atas)." Bisakah Anda benar-benar memberikan beberapa contoh?
perryzheng

2
@perryzheng per ini , mengambil trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)dan def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Maka, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})tidak apa-apa, karena penggembala hewan kita dapat menggembalakan sapi, tetapi iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})memberikan kesalahan kompilasi, karena penggembala sapi kita tidak dapat menggembalakan semua hewan.
Lasf

Ini terkait dan membantu saya dengan varians: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz

27

@Daniel telah menjelaskannya dengan sangat baik. Tetapi untuk menjelaskannya secara singkat, jika diizinkan:

  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.getkemudian akan melempar kesalahan saat runtime karena tidak berhasil dalam mengonversikan Animalke Dog(ya!).

Secara umum, kemampuan berubah tidak cocok dengan ko-varians dan kontra-varians. Itulah alasan mengapa semua koleksi Java adalah invarian.


7

Lihat Scala dengan contoh , halaman 57+ untuk diskusi lengkap tentang ini.

Jika saya memahami komentar Anda dengan benar, Anda perlu membaca ulang bagian yang dimulai di bagian bawah halaman 56 (pada dasarnya, apa yang saya pikir Anda minta tidak aman untuk jenis tanpa menjalankan pemeriksaan waktu berjalan, yang tidak dilakukan scala, jadi kamu kurang beruntung). Menerjemahkan contoh mereka untuk menggunakan konstruk Anda:

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 

Jika Anda merasa saya tidak memahami pertanyaan Anda (kemungkinan berbeda), coba tambahkan lebih banyak penjelasan / konteks ke deskripsi masalah dan saya akan coba lagi.

Menanggapi hasil edit Anda: Slot yang tidak dapat diubah adalah situasi yang sangat berbeda ... * tersenyum * Saya harap contoh di atas membantu.


Saya telah membaca itu; sayangnya saya (masih) tidak mengerti bagaimana saya bisa melakukan apa yang saya minta di atas (yaitu benar-benar menulis kovarian kelas parametrized di T)
oxbow_lakes

Saya menghapus downmark saya karena saya menyadari ini agak keras. Saya seharusnya menjelaskan dalam pertanyaan (s) bahwa saya telah membaca bit dari Scala dengan contoh; Saya hanya ingin dijelaskan dengan cara "kurang formal"
oxbow_lakes

@oxbow_lakes smile Saya takut Scala By Example adalah penjelasan yang kurang formal. Paling-paling, kita dapat mencoba menggunakan contoh nyata untuk bekerja di sini ...
MarkusQ

Maaf - Saya tidak ingin slot saya bisa berubah. Saya baru saja menyadari bahwa masalahnya adalah saya menyatakan var dan tidak val
oxbow_lakes

3

Anda perlu menerapkan batas bawah pada parameter. Saya mengalami kesulitan mengingat sintaks, tetapi saya pikir itu akan terlihat seperti ini:

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

Scala-by-example agak sulit dipahami, beberapa contoh konkret akan membantu.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.