Apa tradeoff untuk inferensi tipe?

29

Tampaknya semua bahasa pemrograman baru atau setidaknya yang menjadi populer menggunakan inferensi tipe. Bahkan Javascript mendapat jenis dan tipe inferensi melalui berbagai implementasi (Acscript, typescript dll). Itu tampak hebat bagi saya tapi saya bertanya-tanya apakah ada trade-off atau mengapa katakanlah Java atau bahasa-bahasa lama yang baik tidak memiliki inferensi jenis

  • Ketika mendeklarasikan variabel di Go tanpa menentukan jenisnya (menggunakan var tanpa tipe atau: = sintaks), tipe variabel disimpulkan dari nilai di sisi kanan.
  • D memungkinkan penulisan fragmen kode besar tanpa menentukan jenis yang berlebihan, seperti yang dilakukan oleh bahasa dinamis. Di sisi lain, inferensi statis menyimpulkan jenis dan properti kode lainnya, memberikan yang terbaik dari dunia statis dan dinamis.
  • Jenis mesin inferensi di Rust cukup cerdas. Itu lebih dari melihat jenis nilai-r selama inisialisasi. Ini juga terlihat bagaimana variabel digunakan setelahnya untuk menyimpulkan tipenya.
  • Swift menggunakan inferensi tipe untuk mengetahui tipe yang sesuai. Ketik inferensi memungkinkan kompiler untuk menyimpulkan jenis ekspresi tertentu secara otomatis ketika mengkompilasi kode Anda, hanya dengan memeriksa nilai-nilai yang Anda berikan.
Pengguna tanpa topi
sumber
3
Dalam C #, pedoman umum mengatakan untuk tidak selalu menggunakan varkarena kadang-kadang dapat merusak keterbacaan.
Mephy
5
"... atau mengapa katakanlah Jawa atau bahasa-bahasa lama yang bagus tidak memiliki tipe inferensi" Alasannya mungkin historis; ML muncul 1 tahun setelah C menurut Wikipedia dan memiliki tipe inferensi. Java sedang mencoba untuk menarik pengembang C ++ . C ++ dimulai sebagai perpanjangan dari C, dan kekhawatiran utama C adalah menjadi pembungkus portabel di atas rakitan dan mudah dikompilasi. Untuk apa pun nilainya, saya telah membaca bahwa subtyping membuat inferensi jenis tidak dapat diputuskan dalam kasus umum juga.
Doval
3
@Doval Scala tampaknya melakukan pekerjaan yang cukup bagus dalam menyimpulkan jenis, untuk bahasa yang mendukung warisan subtipe. Ini tidak sebagus bahasa ML-keluarga, tetapi mungkin sebaik yang Anda bisa minta, mengingat desain bahasa.
KChaloux
12
Ada baiknya untuk menarik perbedaan antara pengurangan tipe (monodirectional, seperti C # vardan C ++ auto) dan tipe inferensi (dua arah, seperti Haskell let). Dalam kasus sebelumnya, jenis nama dapat disimpulkan dari penginisialisasi saja — penggunaannya harus mengikuti jenis nama. Dalam kasus yang terakhir, jenis nama dapat disimpulkan dari penggunaannya juga — yang berguna karena Anda dapat menulis hanya []untuk urutan kosong, terlepas dari jenis elemen, atau newEmptyMVaruntuk referensi nol yang dapat diubah baru, terlepas dari referensi mengetik.
Jon Purdy
Jenis inferensi bisa sangat rumit untuk diterapkan, terutama secara efisien. Orang tidak ingin kompleksitas kompilasi mereka mengalami ledakan eksponensial dari ekspresi yang lebih kompleks. Baca menarik: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Alexander

Jawaban:

43

Sistem tipe Haskell sepenuhnya dapat disimpulkan (mengesampingkan rekursi polimorfik, ekstensi bahasa tertentu , dan pembatasan monomorfisme yang ditakuti ), namun programmer masih sering memberikan anotasi jenis dalam kode sumber bahkan ketika mereka tidak perlu. Mengapa?

  1. Ketik anotasi berfungsi sebagai dokumentasi . Ini terutama benar dengan tipe-tipe yang ekspresif seperti Haskell. Diberi nama fungsi dan jenisnya, Anda biasanya dapat menebak dengan baik apa fungsi tersebut, terutama ketika fungsi tersebut secara parametrik polimorfik.
  2. Jenis anotasi dapat mendorong pengembangan . Menulis tanda tangan jenis sebelum Anda menulis tubuh fungsi terasa seperti pengembangan berbasis tes. Dalam pengalaman saya, setelah Anda membuat fungsi Haskell mengkompilasinya biasanya berfungsi pertama kali. (Tentu saja, ini tidak meniadakan perlunya tes otomatis!)
  3. Jenis eksplisit dapat membantu Anda memeriksa asumsi Anda . Ketika saya mencoba memahami beberapa kode yang sudah berfungsi, saya sering menambahkannya dengan apa yang saya yakini sebagai anotasi jenis yang benar. Jika kode masih dikompilasi, saya tahu saya sudah memahaminya. Jika tidak, saya membaca pesan kesalahan.
  4. Ketik tanda tangan memungkinkan Anda mengkhususkan fungsi polimorfik . Kadang-kadang, API lebih ekspresif atau berguna jika fungsi-fungsi tertentu tidak polimorfik. Kompiler tidak akan mengeluh jika Anda memberikan fungsi jenis yang kurang umum daripada yang akan disimpulkan. Contoh klasiknya adalah map :: (a -> b) -> [a] -> [b]. Bentuk yang lebih umum ( fmap :: Functor f => (a -> b) -> f a -> f b) berlaku untuk semua Functor, bukan hanya daftar. Tetapi dirasakan bahwa mapakan lebih mudah dipahami bagi pemula, sehingga ia hidup bersama kakaknya yang lebih besar.

Secara keseluruhan, kelemahan dari sistem yang diketik secara statis tetapi dapat disimpulkan sama dengan kelemahan dari pengetikan statis secara umum, diskusi usang di situs ini dan yang lainnya (Googling "kekurangan pengetikan statis" akan membuat Anda ratusan halaman nyala api). Tentu saja, beberapa kerugian tersebut diperbaiki dengan jumlah anotasi jenis yang lebih kecil dalam sistem yang dapat disimpulkan. Plus, inferensi tipe memiliki kelebihannya sendiri: pengembangan hole-driven tidak akan mungkin tanpa inferensi tipe.

Java * membuktikan bahwa bahasa yang membutuhkan terlalu banyak anotasi jenis menjadi menjengkelkan, tetapi dengan terlalu sedikit Anda kehilangan kelebihan yang saya jelaskan di atas. Bahasa-bahasa dengan inferensi penyisihan keluar-jenis memberikan keseimbangan yang disetujui antara kedua ekstrem.

* Bahkan Java, kambing hitam yang hebat, melakukan sejumlah inferensi tipe lokal . Dalam pernyataan seperti Map<String, Integer> = new HashMap<>();, Anda tidak harus menentukan tipe generik konstruktor. Di sisi lain, bahasa gaya-ML biasanya tidak dapat ditebak secara global .

Benjamin Hodgson
sumber
2
Tapi Anda bukan pemula;) Anda harus memiliki pemahaman tentang kelas jenis dan jenis sebelum Anda benar-benar "mengerti" Functor. Daftar dan maplebih mungkin akrab dengan Haskellers yang tidak berpengalaman.
Benjamin Hodgson
1
Apakah Anda menganggap C # varsebagai contoh inferensi tipe?
Benjamin Hodgson
1
Ya, C # varadalah contoh yang tepat.
Doval
1
Situs ini sudah menandai diskusi ini terlalu panjang sehingga ini akan menjadi jawaban terakhir saya. Dalam yang pertama kompiler harus menentukan jenis x; dalam yang terakhir tidak ada tipe untuk menentukan, semua tipe diketahui dan Anda hanya perlu memeriksa bahwa ungkapan itu masuk akal. Perbedaannya menjadi lebih penting ketika Anda melewati contoh-contoh sepele dan xdigunakan di banyak tempat; kompiler kemudian harus memeriksa ulang lokasi di mana xdigunakan untuk menentukan 1) apakah mungkin untuk menetapkan xjenis sehingga kode akan mengetik cek? 2) Jika ya, jenis apa yang paling umum yang dapat kita tetapkan?
Doval
1
Perhatikan bahwa new HashMap<>();sintaks hanya ditambahkan di Java 7, dan lambdas di Java 8 memungkinkan untuk banyak inferensi tipe "nyata".
Michael Borgwardt
8

Dalam C #, ketik inferensi terjadi pada waktu kompilasi, sehingga biaya runtime adalah nol.

Sebagai gaya, vardigunakan untuk situasi di mana tidak nyaman atau tidak perlu untuk menentukan jenisnya secara manual. Linq adalah salah satu situasi seperti itu. Lainnya adalah:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

tanpanya Anda akan mengulang nama tipe yang sangat panjang (dan parameter tipe) daripada hanya mengatakan var.

Gunakan nama sebenarnya dari tipe ini ketika secara eksplisit meningkatkan kejelasan kode.

Ada beberapa situasi di mana tipe inferensi tidak dapat digunakan, seperti deklarasi variabel anggota yang nilainya ditetapkan pada waktu konstruksi, atau di mana Anda benar-benar ingin intellisense berfungsi dengan baik (Hackerrank's IDE tidak akan menginternisikan anggota variabel kecuali jika Anda menyatakan jenisnya secara eksplisit) .

Robert Harvey
sumber
16
"Dalam C #, ketik inferensi terjadi pada waktu kompilasi, jadi biaya runtime adalah nol." Ketik inferensi terjadi pada waktu kompilasi menurut definisi, jadi itulah yang terjadi di setiap bahasa.
Doval
Jauh lebih baik.
Robert Harvey
1
@Doval dimungkinkan untuk melakukan inferensi tipe selama kompilasi JIT, yang jelas memiliki biaya runtime.
Jules
6
@ Jules Jika Anda sudah sejauh menjalankan program maka sudah ketik diperiksa; tidak ada yang tersisa untuk disimpulkan. Apa yang Anda bicarakan biasanya tidak disebut inferensi jenis, meskipun saya tidak yakin apa istilah yang tepat.
Doval
7

Pertanyaan bagus!

  1. Karena jenisnya tidak dianotasi secara eksplisit, kadang-kadang dapat membuat kode lebih sulit dibaca - mengarah ke lebih banyak bug. Menggunakannya dengan benar tentu saja membuat kode lebih bersih dan lebih mudah dibaca. Jika Anda pesimis dan berpikir bahwa kebanyakan programmer yang buruk (atau bekerja di mana kebanyakan programmer yang buruk), ini akan menjadi kerugian bersih.
  2. Meskipun jenis algoritma inferensi relatif sederhana, itu tidak gratis . Hal semacam ini meningkatkan waktu kompilasi sedikit.
  3. Karena jenisnya tidak dijelaskan secara eksplisit, IDE Anda tidak dapat menebak dengan baik apa yang Anda coba lakukan, merusak pelengkapan otomatis dan bantuan serupa selama proses deklarasi.
  4. Dikombinasikan dengan kelebihan fungsi, Anda kadang-kadang bisa masuk ke situasi di mana algoritma tipe inferensi tidak dapat memutuskan jalur yang akan diambil, mengarah ke anotasi gaya casting yang lebih buruk. (Ini terjadi cukup banyak dengan sintaks fungsi anonim C # misalnya).

Dan ada lebih banyak bahasa esoterik yang tidak dapat melakukan keanehan mereka tanpa anotasi jenis eksplisit. Sejauh ini, tidak ada yang saya tahu yang umum / populer / cukup menjanjikan untuk disebutkan kecuali secara sepintas.

Telastyn
sumber
1
autocomplete tidak peduli tentang jenis inferensi. Tidak masalah bagaimana tipe itu diputuskan, hanya tipe apa yang dimiliki. Dan masalah dengan lambda C # tidak ada hubungannya dengan inferensi, mereka karena lambda adalah polimorfik tetapi sistem tipe tidak dapat mengatasinya - yang tidak ada hubungannya dengan inferensi.
DeadMG
2
@deadmg - tentu saja, setelah variabel dideklarasikan, ia tidak memiliki masalah, tetapi saat Anda mengetikkan deklarasi variabel, ia tidak dapat mempersempit opsi sisi kanan ke tipe yang dideklarasikan karena tidak ada tipe yang dideklarasikan.
Telastyn
@deadmg - untuk lambda, saya tidak super jelas tentang apa yang Anda maksud, tapi saya cukup yakin itu tidak sepenuhnya benar berdasarkan pada pemahaman saya tentang cara kerja. Bahkan sesuatu yang sesederhana var foo = x => x;gagal karena bahasa perlu disimpulkan di xsini dan tidak ada yang perlu dilanjutkan. Ketika lambda dibangun, mereka dibangun sebagai delegasi yang diketik secara eksplisit Func<int, int> foo = x => xdibangun ke dalam CIL sebagai Func<int, int> foo = new Func<int, int>(x=>x);tempat lambda dibangun sebagai fungsi yang dihasilkan, yang diketik secara eksplisit. Bagi saya, menyimpulkan tipe xbagian dan paket inferensi tipe ...
Telastyn
3
@ Telastyn Tidak benar bahwa bahasa tidak memiliki apa-apa untuk melanjutkan. Semua informasi yang diperlukan untuk mengetik ekspresi x => xterkandung di dalam lambda itu sendiri - itu tidak merujuk ke variabel apa pun dari lingkup sekitarnya. Bahasa fungsional yang waras akan menyimpulkan dengan benar bahwa tipenya adalah "untuk semua jenis a, a -> a". Saya pikir apa yang ingin dikatakan oleh DeadMG adalah bahwa sistem tipe C # tidak memiliki properti tipe utama yang berarti bahwa selalu mungkin untuk mengetahui tipe yang paling umum untuk ekspresi apa pun. Ini properti yang sangat mudah dihancurkan.
Doval
@Doval - meningkatkan ... tidak ada yang namanya objek fungsi generik di C #, yang menurut saya agak berbeda dari apa yang Anda berdua bicarakan bahkan jika itu mengarah ke masalah yang sama.
Telastyn
4

Itu tampak hebat bagi saya tapi saya bertanya-tanya apakah ada trade-off atau mengapa katakanlah Java atau bahasa-bahasa lama yang baik tidak memiliki inferensi jenis

Kebetulan Java menjadi pengecualian daripada aturan di sini. Bahkan C ++ (yang saya percaya memenuhi syarat sebagai "bahasa tua yang baik" :)) mendukung inferensi jenis dengan autokata kunci sejak standar C ++ 11. Ini tidak hanya bekerja dengan deklarasi variabel, tetapi juga sebagai tipe fungsi pengembalian, yang sangat berguna dengan beberapa fungsi template yang kompleks.

Mengetik dan mengetik inferensi implisit memiliki banyak kasus penggunaan yang baik, dan ada juga beberapa kasus penggunaan di mana Anda benar-benar tidak boleh melakukannya. Ini terkadang masalah selera, dan juga menjadi bahan perdebatan.

Tetapi tidak diragukan lagi bahwa ada kasus penggunaan yang baik, dengan sendirinya merupakan alasan yang sah untuk bahasa apa pun untuk mengimplementasikannya. Ini juga bukan fitur yang sulit untuk diimplementasikan, tidak ada penalti runtime, dan tidak mempengaruhi waktu kompilasi secara signifikan.

Saya tidak melihat kelemahan nyata dalam memberikan kesempatan kepada pengembang untuk menggunakan inferensi tipe.

Beberapa penjawab menimbang-nebak seberapa baik mengetik eksplisit itu baik, dan tentu saja itu benar. Tetapi tidak mendukung pengetikan tersirat akan berarti bahwa suatu bahasa memaksakan pengetikan eksplisit sepanjang waktu.

Jadi penarikan sebenarnya adalah ketika sebuah bahasa tidak mendukung pengetikan tersirat, karena dengan ini menyatakan, bahwa tidak ada alasan yang sah bagi pengembang untuk menggunakannya, yang tidak benar.

Gábor Angyal
sumber
1

Perbedaan utama antara sistem inferensi tipe Hindley-Milner dan inferensi tipe Go-style adalah arah aliran informasi. Di HM, ketik informasi mengalir maju dan mundur melalui penyatuan; di Go, ketik hanya arus informasi yang mengalir: ia menghitung maju hanya penggantian.

Inferensi tipe HM adalah inovasi hebat yang bekerja dengan baik dengan bahasa fungsional yang diketik secara polimorfik, tetapi penulis Go mungkin berpendapat bahwa ini mencoba melakukan terlalu banyak:

  1. Fakta bahwa informasi mengalir baik ke depan maupun ke belakang berarti bahwa inferensi tipe HM adalah urusan yang sangat non-lokal; jika tidak ada anotasi jenis, setiap baris kode dalam program Anda dapat berkontribusi pada pengetikan satu baris kode. Jika Anda hanya memiliki substitusi maju, Anda tahu bahwa sumber kesalahan tipe harus dalam kode yang mendahului kesalahan Anda; bukan kode yang muncul setelahnya.

  2. Dengan inferensi tipe HM, Anda cenderung berpikir dalam kendala: ketika Anda menggunakan suatu jenis, Anda membatasi jenis apa yang mungkin dimiliki. Pada akhirnya, mungkin ada beberapa variabel tipe yang dibiarkan sama sekali tidak dibatasi. Wawasan dari inferensi tipe HM adalah bahwa, tipe-tipe itu benar-benar tidak penting, sehingga mereka dibuat menjadi variabel polimorfik. Namun, polimorfisme ekstra ini dapat tidak diinginkan karena sejumlah alasan. Pertama, seperti yang ditunjukkan oleh beberapa orang, polimorfisme ekstra ini mungkin tidak diinginkan: HM menyimpulkan tipe palsu, polimorfik untuk beberapa kode palsu, dan menyebabkan kesalahan aneh nantinya. Kedua, ketika suatu tipe dibiarkan polimorfik, ini dapat memiliki konsekuensi untuk perilaku runtime. Misalnya, hasil antara yang terlalu polimorfik adalah alasan mengapa 'ditampilkan. baca 'dianggap ambigu dalam Haskell; sebagai contoh lain,

Edward Z. Yang
sumber
2
Sayangnya, ini sepertinya jawaban yang bagus untuk pertanyaan yang berbeda. OP bertanya tentang "In-style type inference" versus bahasa yang tidak memiliki inferensi tipe apa pun, bukan "Go-style" versus HM.
Ixrec
-2

Itu menyakitkan keterbacaan.

Membandingkan

var stuff = someComplexFunction()

vs.

List<BusinessObject> stuff = someComplexFunction()

Terutama masalah ketika membaca di luar IDE seperti di github.

miguel
sumber
4
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 6 jawaban sebelumnya
nyamuk
Jika Anda menggunakan nama yang tepat alih-alih "kompleks tidak dapat dibaca", maka vardapat digunakan tanpa merusak keterbacaan. var objects = GetBusinessObjects();
Fabio
Ok tapi kemudian Anda membocorkan sistem tipe ke nama variabel. Ini seperti notasi hungaria. Setelah refactor, Anda cenderung berakhir dengan nama yang tidak cocok dengan kode. Secara umum, gaya fungsional mendorong Anda untuk kehilangan keunggulan penempatan nama alami yang disediakan kelas. Jadi, Anda berakhir dengan lebih banyak squareArea () daripada square.area ().
miguel