Sistem tipe generik yang bagus

29

Sudah umum diterima bahwa generik Java gagal dalam beberapa hal penting. Kombinasi kartu liar dan batas menyebabkan beberapa kode serius tidak dapat dibaca.

Namun, ketika saya melihat bahasa lain, saya benar-benar tidak bisa menemukan sistem tipe generik yang disukai oleh para programmer.

Jika kami mengambil yang berikut sebagai sasaran desain sistem tipe seperti itu:

  • Selalu menghasilkan deklarasi tipe yang mudah dibaca
  • Mudah dipelajari (tidak perlu memoles kovarians, contravariance, dll.)
  • memaksimalkan jumlah kesalahan waktu kompilasi

Apakah ada bahasa yang benar? Jika saya google, satu-satunya hal yang saya lihat adalah keluhan tentang bagaimana sistem jenis mengisap dalam bahasa X. Apakah kompleksitas semacam ini melekat dalam pengetikan generik? Haruskah kita menyerah untuk mencoba memverifikasi keamanan tipe 100% pada waktu kompilasi?

Pertanyaan utama saya adalah bahasa mana yang “paling benar” yang terbaik sehubungan dengan ketiga tujuan ini. Saya menyadari bahwa itu subjektif, tetapi sejauh ini saya bahkan tidak dapat menemukan satu bahasa di mana tidak semua programmer setuju bahwa sistem tipe generik berantakan.

Tambahan: seperti yang disebutkan, kombinasi subtyping / inheritance dan generik adalah yang menciptakan kompleksitas, jadi saya benar-benar mencari bahasa yang menggabungkan keduanya dan menghindari ledakan kompleksitas.

Peter
sumber
2
Apa maksudmu easy-to-read type declarations? Kriteria ketiga juga ambigu: misalnya, saya dapat mengubah indeks array dari batas menjadi kesalahan waktu kompilasi dengan tidak membiarkan Anda indeks array kecuali saya dapat menghitung indeks pada waktu kompilasi. Juga, kriteria kedua mengesampingkan subtyping. Itu tidak selalu merupakan hal yang buruk tetapi Anda harus menyadari apa yang Anda minta.
Doval
17
Lihat sebagian besar bahasa fungsional
AlexFoxGill
9
@gnat, ini jelas bukan kata-kata kasar terhadap Java. Program saya hampir secara eksklusif di Jawa. Maksud saya adalah bahwa secara umum diterima dalam komunitas Java bahwa Generics cacat (bukan kegagalan total, tetapi mungkin sebagian), jadi pertanyaan logis untuk bertanya bagaimana seharusnya diterapkan. Mengapa mereka salah dan apakah orang lain memperbaikinya? Atau apakah benar-benar mustahil mendapatkan obat generik yang benar?
Peter
1
Apakah semua orang hanya mencuri dari C # akan ada lebih sedikit keluhan. Terutama Jawa dalam posisi untuk mengejar ketinggalan dengan menyalin. Sebaliknya mereka memutuskan solusi yang lebih rendah. Banyak pertanyaan yang masih dibahas oleh komite desain Java telah diputuskan dan diimplementasikan dalam C #. Mereka bahkan tidak terlihat.
usr
2
@emodendroket: Saya pikir dua keluhan terbesar saya tentang C # generics adalah bahwa tidak ada cara untuk menerapkan batasan "supertype" (mis. Foo<T> where SiameseCat:T) dan bahwa tidak ada kemungkinan memiliki tipe generik yang tidak dapat dikonversi Object. IMHO, .NET akan mendapat manfaat dari tipe agregat yang mirip dengan struktur, tetapi bahkan lebih bertulang. Jika KeyValuePair<TKey,TValue>jenis seperti itu, maka sebuah IEnumerable<KeyValuePair<SiameseCat,FordFocus>>dapat dilemparkan ke IEnumerable<KeyValuePair<Animal,Vehicle>>, tetapi hanya jika jenisnya tidak bisa kotak.
supercat

Jawaban:

24

Sementara Generik telah menjadi arus utama dalam komunitas pemrograman fungsional selama beberapa dekade, menambahkan generik ke bahasa pemrograman berorientasi objek menawarkan beberapa tantangan unik, khususnya interaksi subtyping dan generik.

Namun, bahkan jika kita fokus pada bahasa pemrograman berorientasi objek, dan Java khususnya, sistem generik yang jauh lebih baik dapat dirancang:

  1. Jenis generik harus diterima di mana pun jenis lainnya. Secara khusus, jika Tadalah parameter tipe, ekspresi berikut harus dikompilasi tanpa peringatan:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Ya, ini membutuhkan obat generik untuk diverifikasi, sama seperti setiap jenis bahasa lainnya.

  2. Kovarian dan contravariance dari tipe generik harus ditentukan dalam (atau disimpulkan dari) deklarasi, daripada setiap kali tipe generik digunakan, sehingga kita dapat menulis

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    daripada

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Karena tipe generik dapat menjadi agak lama, kita tidak perlu menentukannya secara berlebihan. Artinya, kita harus bisa menulis

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    daripada

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Jenis apa pun harus diterima sebagai parameter tipe, bukan hanya tipe referensi. (Jika kita dapat memiliki int[], mengapa kita tidak dapat memiliki List<int>)?

Semua ini dimungkinkan dalam C #.

meriton - mogok
sumber
1
Apakah ini juga akan menghilangkan obat rujukan diri? Bagaimana jika saya ingin mengatakan bahwa objek yang sebanding dapat membandingkan dirinya sendiri dengan apa pun dari jenis yang sama atau subkelas? Bisakah itu dilakukan? Atau jika saya menulis metode pengurutan yang menerima daftar dengan objek yang sebanding, semua harus sebanding satu sama lain. Enum adalah contoh bagus lainnya: Enum <E extends Enum <E>>. Saya tidak mengatakan sistem tipe harus dapat melakukan ini, saya hanya ingin tahu bagaimana C # menangani situasi ini.
Peter
1
Inferensi tipe generik Java 7 dan bantuan otomatis C ++ untuk sebagian masalah ini, tetapi bersifat sintaksis dan tidak mengubah mekanisme yang mendasarinya.
@Snowman inferensi tipe Java memiliki beberapa kasus sudut yang benar-benar menjengkelkan, seperti tidak bekerja dengan kelas anonim sama sekali dan tidak menemukan batas yang tepat untuk wildcard ketika Anda mengevaluasi metode generik sebagai argumen ke metode generik lain.
Doval
@Doval itu sebabnya saya mengatakan itu membantu dengan beberapa masalah: tidak memperbaiki apa-apa, dan tidak menangani semuanya. Java generik memiliki banyak masalah: sementara lebih baik daripada jenis mentah, mereka tentu saja menyebabkan banyak sakit kepala.
34

Penggunaan subtipe menciptakan banyak komplikasi ketika melakukan pemrograman generik. Jika Anda bersikeras menggunakan bahasa dengan subtipe, Anda harus menerima ada kompleksitas inheren tertentu dalam pemrograman generik yang menyertainya. Beberapa bahasa melakukannya lebih baik daripada yang lain, tetapi Anda hanya bisa mengambil sejauh ini.

Bandingkan dengan generik Haskell, misalnya. Mereka cukup sederhana sehingga jika Anda menggunakan tipe inferensi, Anda dapat menulis fungsi generik yang benar secara tidak sengaja . Bahkan, jika Anda menentukan jenis tunggal, compiler sering berkata kepada dirinya sendiri, "Yah, aku sedang akan membuat ini generik, tetapi Anda meminta saya untuk membuatnya hanya untuk ints, jadi apa pun."

Diakui, orang menggunakan sistem tipe Haskell dalam beberapa cara yang sangat rumit, menjadikannya kutukan bagi setiap pemula, tetapi sistem tipe yang mendasarinya sendiri elegan dan sangat dikagumi.

Karl Bielefeldt
sumber
1
Terima kasih atas jawaban ini. Artikel ini dimulai dengan beberapa contoh Joshua Bloch di mana obat generik menjadi terlalu rumit: artima.com/weblogs/viewpost.jsp?thread=222021 . Apakah ini perbedaan budaya antara Jawa dan Haskell, di mana konstruksi seperti itu akan dianggap baik-baik saja di Haskell, atau apakah ada perbedaan nyata dengan sistem tipe Haskell yang menghindari situasi seperti itu?
Peter
10
@Peter Haskell tidak memiliki subtyping, dan seperti Karl mengatakan kompiler dapat menyimpulkan jenis secara otomatis termasuk kendala seperti "tipe aharus semacam integer".
Doval
Dengan kata lain kovarians , dalam bahasa seperti Scala.
Paul Draper
14

Ada sedikit riset dalam menggabungkan obat generik dengan subtyping yang terjadi sekitar 20 tahun yang lalu. Bahasa pemrograman Thor yang dikembangkan oleh kelompok riset Barbara Liskov di MIT memiliki gagasan tentang klausa "di mana" yang memungkinkan Anda menentukan persyaratan jenis yang Anda parameterkan. (Ini mirip dengan apa yang C ++ coba lakukan dengan Konsep .)

Makalah yang menggambarkan generik Thor dan bagaimana mereka berinteraksi dengan subtipe Thor adalah: Day, M; Gruber, R; Liskov, B; Myers, AC: Subtipe vs. klausa mana: membatasi polimorfisme parametrik , ACM Conf pada Obj-Oriented Prog, Sys, Lang dan Apps , (OOPSLA-10): 156-158, 1995.

Saya percaya mereka, pada gilirannya, dibangun di atas pekerjaan yang dilakukan di Emerald selama akhir 1980-an. (Saya belum membaca karya itu, tetapi rujukannya adalah: Hitam, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribusi dan Jenis Abstrak di Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Baik Thor maupun Emerald adalah "bahasa akademik" sehingga mereka mungkin tidak mendapatkan penggunaan yang cukup bagi orang-orang untuk benar-benar memahami apakah klausa (konsep) benar-benar memecahkan masalah nyata. Sangat menarik untuk membaca artikel Bjarne Stroustrup tentang mengapa percobaan pertama di Concepts in C ++ gagal: Stroustrup, B: Keputusan C ++ 0x "Remove Concepts" , Dr Dobbs , 22 Juli 2009. (Info lebih lanjut tentang halaman muka Stroustrup . )

Arah lain yang tampaknya orang coba adalah sesuatu yang disebut ciri . Misalnya bahasa pemrograman Rust Mozilla menggunakan ciri-ciri. Seperti yang saya pahami (yang mungkin benar-benar salah), menyatakan bahwa suatu kelas memuaskan suatu sifat sangat mirip dengan mengatakan bahwa suatu kelas mengimplementasikan antarmuka, tetapi Anda mengatakan "berperilaku seperti" daripada "adalah". Tampaknya bahasa pemrograman Swift baru Apple menggunakan konsep protokol yang mirip untuk menetapkan batasan pada parameter ke generik .

Logika Pengembaraan
sumber