Apa sebenarnya yang membuat sistem jenis Haskell begitu dihormati (vs katakanlah, Java)?

204

Saya mulai belajar Haskell . Saya sangat baru dalam hal ini, dan saya hanya membaca beberapa buku online untuk memahami konstruksi dasarnya.

Salah satu 'meme' yang sering dibicarakan orang-orang, adalah keseluruhan "jika dikompilasi, itu akan berfungsi *" - yang saya pikir terkait dengan kekuatan sistem tipe.

Saya mencoba untuk memahami mengapa persis Haskell lebih baik dari lainnya bahasa statis diketik dalam hal ini.

Dengan kata lain, saya berasumsi di Jawa, Anda bisa melakukan sesuatu yang keji seperti mengubur ArrayList<String>()untuk mengandung sesuatu yang seharusnya ArrayList<Animal>(). Hal keji di sini adalah adalah bahwa Anda stringmengandung elephant, giraffe, dll, dan jika seseorang menempatkan di Mercedes- compiler Anda tidak akan membantu Anda.

Jika saya melakukan lakukan ArrayList<Animal>()kemudian, di beberapa titik kemudian dalam waktu, jika saya memutuskan program saya tidak benar-benar tentang hewan, ini tentang kendaraan, maka saya dapat mengubah, katakanlah, sebuah fungsi yang menghasilkan ArrayList<Animal>untuk menghasilkan ArrayList<Vehicle>dan IDE saya harus memberitahu saya di mana-mana ada adalah istirahat kompilasi.

Asumsi saya adalah bahwa inilah yang orang maksud dengan sistem tipe yang kuat , tetapi tidak jelas bagi saya mengapa Haskell lebih baik. Dengan kata lain, Anda dapat menulis Java yang baik atau buruk, saya berasumsi Anda dapat melakukan hal yang sama di Haskell (yaitu memasukkan hal-hal ke dalam string / int yang seharusnya benar-benar tipe data kelas satu).

Saya curiga saya kehilangan sesuatu yang penting / mendasar.
Saya akan sangat senang ditunjukkan kesalahan cara saya!

Phatmanace
sumber
31
Saya akan membiarkan orang lebih berpengetahuan daripada saya menulis jawaban nyata, tetapi intinya adalah ini: bahasa yang diketik secara statis seperti C # memiliki sistem tipe yang berusaha membantu Anda untuk menulis kode yang dapat dipertahankan ; ketik sistem seperti upaya Haskell untuk membantu Anda menulis kode yang benar (dapat dibuktikan). Prinsip dasar di tempat kerja adalah memindahkan hal-hal yang dapat diperiksa ke dalam tahap kompilasi; Haskell memeriksa lebih banyak hal pada waktu kompilasi.
Robert Harvey
8
Saya tidak tahu banyak tentang Haskell, tetapi saya bisa berbicara tentang Jawa. Meskipun terlihat sangat diketik, itu masih memungkinkan Anda untuk melakukan hal-hal "keji" seperti yang Anda katakan. Untuk hampir setiap jaminan yang dibuat Java mengenai sistem tipenya, ada jalan lain.
12
Saya tidak tahu mengapa semua jawaban Maybehanya menyebutkan menjelang akhir. Jika saya harus memilih satu hal yang sebaiknya dipinjam dari bahasa populer dari Haskell, ini dia. Itu ide yang sangat sederhana (jadi tidak terlalu menarik dari sudut pandang teoritis), tetapi ini saja akan membuat pekerjaan kita jauh lebih mudah.
Paul
1
Akan ada jawaban yang bagus di sini, tetapi dalam upaya membantu, pelajari tanda tangan jenis. Mereka memungkinkan manusia dan program untuk beralasan tentang program dengan cara yang akan menggambarkan bagaimana Java berada di tengah-tengah.
Michael Easter
6
Untuk keadilan saya harus menunjukkan bahwa "keseluruhan jika dikompilasi, itu akan berhasil" adalah slogan bukan pernyataan fakta. Ya, kami para programmer Haskell tahu bahwa melewatinya pemeriksa tipe memberi peluang yang baik untuk kebenaran, untuk beberapa gagasan terbatas tentang kebenaran, tetapi tentu saja itu bukan pernyataan "benar" yang secara harfiah dan harfiah!
Tom Ellis

Jawaban:

230

Berikut adalah daftar fitur sistem tipe yang tidak teratur yang tersedia di Haskell dan tidak tersedia atau kurang bagus di Jawa (sepengetahuan saya, Java wrt yang diakui lemah)

  • Keselamatan . Tipe Haskell memiliki properti "tipe safety" yang cukup bagus. Ini cukup spesifik, tetapi pada dasarnya berarti bahwa nilai-nilai pada suatu tipe tidak dapat diubah menjadi tipe lain. Ini terkadang bertentangan dengan kemampuan berubah-ubah (lihat pembatasan nilai OCaml )
  • Tipe Data aljabar . Jenis-jenis dalam Haskell pada dasarnya memiliki struktur yang sama dengan matematika SMA. Ini sangat sederhana dan konsisten, namun, ternyata, sekuat yang Anda inginkan. Ini hanyalah dasar yang bagus untuk sistem tipe.
    • Pemrograman Datatype-generik . Ini tidak sama dengan tipe generik (lihat generalisasi ). Sebaliknya, karena kesederhanaan struktur tipe seperti yang dicatat sebelumnya, relatif mudah untuk menulis kode yang beroperasi secara umum di atas struktur itu. Kemudian saya berbicara tentang bagaimana sesuatu seperti Equality mungkin diturunkan secara otomatis untuk tipe yang ditentukan pengguna oleh kompiler Haskell. Pada dasarnya cara melakukan hal ini adalah berjalan di atas struktur umum, sederhana yang mendasari setiap jenis yang ditetapkan pengguna dan mencocokkannya di antara nilai-nilai — bentuk yang sangat alami dari kesetaraan struktural.
  • Jenis yang saling rekursif . Ini hanya komponen penting dari penulisan jenis-jenis yang tidak sepele.
    • Jenis bersarang . Ini memungkinkan Anda untuk menentukan tipe rekursif atas variabel yang berulang pada tipe yang berbeda. Misalnya, satu jenis pohon seimbang adalah data Bt a = Here a | There (Bt (a, a)). Pikirkan baik-baik tentang nilai valid Bt adan perhatikan bagaimana jenis itu bekerja. Ini rumit!
  • Generalisasi . Ini hampir terlalu konyol untuk tidak ada dalam sistem tipe (ahem, melihatmu, Go). Sangat penting untuk memiliki gagasan tentang variabel tipe dan kemampuan untuk berbicara tentang kode yang terlepas dari pilihan variabel itu. Hindley Milner adalah sistem tipe yang berasal dari Sistem F. Sistem tipe Haskell adalah penjabaran dari pengetikan HM dan Sistem F pada dasarnya adalah jantung dari generalisasi. Yang ingin saya katakan adalah bahwa Haskell memiliki kisah generalisasi yang sangat bagus .
  • Jenis abstrak . Kisah Haskell di sini tidak bagus tapi juga tidak ada. Mungkin untuk menulis jenis yang memiliki antarmuka publik tetapi implementasi pribadi. Ini memungkinkan kita untuk mengakui perubahan pada kode implementasi di lain waktu dan, yang penting karena ini adalah dasar dari semua operasi di Haskell, tulis tipe "ajaib" yang memiliki antarmuka yang didefinisikan dengan baik seperti IO. Java mungkin sebenarnya memiliki tipe cerita abstrak yang lebih bagus, jujur ​​saja, tapi saya tidak berpikir sampai Interfaces menjadi lebih populer adalah itu benar-benar benar.
  • Parametrik . Nilai-nilai Haskell tidak memiliki setiap operasi universal. Java melanggar ini dengan hal-hal seperti referensi kesetaraan dan hashing dan bahkan lebih mencolok dengan paksaan. Artinya, Anda mendapatkan teorema gratis tentang tipe yang memungkinkan Anda mengetahui arti suatu operasi atau nilai hingga tingkat yang luar biasa sepenuhnya dari jenisnya --- tipe tertentu sedemikian rupa sehingga hanya akan ada sejumlah kecil penduduk.
  • Jenis yang lebih tinggi menunjukkan semua jenis saat menyandikan hal-hal yang lebih rumit. Functor / Applicative / Monad, Foldable / Traversable, seluruh mtlsistem pengetikan efek, fixpoints functor umum. Daftar ini terus berlanjut. Ada banyak hal yang paling baik diungkapkan pada jenis yang lebih tinggi dan sistem jenis yang relatif sedikit bahkan memungkinkan pengguna untuk membicarakan hal-hal ini.
  • Ketik kelas . Jika Anda menganggap sistem ketik sebagai logika — yang berguna — maka Anda sering diminta untuk membuktikan sesuatu. Dalam banyak kasus ini pada dasarnya adalah derau baris: mungkin hanya ada satu jawaban yang benar dan itu adalah buang-buang waktu dan upaya bagi programmer untuk menyatakan ini. Typeclasses adalah cara bagi Haskell untuk menghasilkan bukti untuk Anda. Dalam istilah yang lebih konkret, ini memungkinkan Anda memecahkan "sistem persamaan tipe" sederhana seperti "Di tipe mana kita berniat untuk (+)bersama-sama? Oh Integer, ok! Mari kita sebariskan kode yang benar sekarang!". Pada sistem yang lebih kompleks, Anda mungkin membuat batasan yang lebih menarik.
    • Batasi kalkulus . Batasan dalam Haskell — yang merupakan mekanisme untuk mencapai ke dalam sistem prolog typeclass — diketik secara struktural. Ini memberikan bentuk hubungan subtyping yang sangat sederhana yang memungkinkan Anda mengumpulkan kendala kompleks dari yang lebih sederhana. Seluruh mtlperpustakaan didasarkan pada ide ini.
    • Diturunkan . Untuk menggerakkan kanonisitas sistem typeclass, penting untuk menulis banyak kode yang sering sepele untuk menggambarkan kendala yang harus ditentukan oleh tipe yang ditentukan pengguna. Lakukan pada struktur tipe Haskell yang sangat normal, sering kali Anda dapat meminta kompiler untuk melakukan boilerplate ini untuk Anda.
    • Ketik prolog kelas . Pemecah kelas tipe Haskell - sistem yang menghasilkan "bukti" yang saya sebutkan sebelumnya - pada dasarnya adalah bentuk Prolog yang pincang dengan sifat semantik yang lebih bagus. Ini berarti Anda dapat menyandikan hal-hal yang sangat berbulu dalam jenis prolog dan mengharapkannya ditangani semua pada waktu kompilasi. Contoh yang baik mungkin memecahkan untuk bukti bahwa dua daftar heterogen adalah setara jika Anda lupa tentang urutannya — mereka adalah "set" heterogen yang setara.
    • Kelas tipe multi-parameter dan dependensi fungsional . Ini hanyalah penyempurnaan yang sangat berguna untuk mendasarkan prolog typeclass. Jika Anda tahu Prolog, Anda bisa membayangkan berapa banyak daya ekspresif meningkat ketika Anda dapat menulis predikat lebih dari satu variabel.
  • Inferensi yang cukup bagus . Bahasa yang didasarkan pada sistem jenis Hindley Milner memiliki inferensi yang cukup bagus. HM itu sendiri memiliki inferensi lengkap yang berarti bahwa Anda tidak perlu menulis variabel tipe. Haskell 98, bentuk paling sederhana dari Haskell, sudah membuang itu dalam beberapa keadaan yang sangat langka. Secara umum, Haskell modern telah menjadi percobaan dalam perlahan-lahan mengurangi ruang inferensi lengkap sambil menambahkan lebih banyak kekuatan untuk HM dan melihat ketika pengguna mengeluh. Orang-orang jarang mengeluh — kesimpulan Haskell cukup bagus.
  • Subtiping sangat, sangat, sangat lemah . Saya sebutkan sebelumnya bahwa sistem kendala dari typeclass prolog memiliki gagasan tentang subtyping struktural. Itu adalah satu-satunya bentuk subtyping di Haskell . Subtyping itu buruk untuk alasan dan kesimpulan. Itu membuat masing-masing masalah secara signifikan lebih sulit (sistem ketidaksetaraan, bukan sistem kesetaraan). Ini juga sangat mudah untuk disalahpahami (Apakah subklasifikasi sama dengan subtyping? Tentu saja tidak! Tetapi orang-orang sangat sering membingungkan itu dan banyak bahasa membantu dalam kebingungan itu! Bagaimana kita berakhir di sini? Saya kira tidak ada yang pernah memeriksa LSP.)
    • Catatan baru-baru ini (awal 2017) Steven Dolan menerbitkan tesisnya tentang MLsub , varian dari inferensi tipe ML dan Hindley-Milner yang memiliki kisah subtipe yang sangat bagus ( lihat juga ). Ini tidak meniadakan apa yang saya tulis di atas — sebagian besar sistem subtyping rusak dan memiliki inferensi buruk - tetapi memang menunjukkan bahwa kita hari ini mungkin telah menemukan beberapa cara yang menjanjikan untuk memiliki inferensi lengkap dan subtipe bermain bersama dengan baik. Sekarang, untuk benar-benar jelas, pengertian Java tentang subtyping sama sekali tidak dapat memanfaatkan algoritma dan sistem Dolan. Ini membutuhkan pemikiran ulang tentang apa arti subtyping.
  • Jenis peringkat yang lebih tinggi . Saya berbicara tentang generalisasi sebelumnya, tetapi lebih dari sekadar generalisasi itu berguna untuk dapat berbicara tentang jenis yang memiliki variabel umum di dalamnya . Misalnya, pemetaan antara struktur orde tinggi yang tidak diketahui (lihat parametrisitas ) dengan apa yang "mengandung" struktur memiliki tipe seperti (forall a. f a -> g a). Dalam lurus HM Anda dapat menulis sebuah fungsi di jenis ini, namun dengan jenis yang lebih tinggi-peringkat Anda menuntut fungsi seperti sebagai argumen seperti: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Perhatikan bahwa avariabel hanya terikat dalam argumen. Ini berarti bahwa definisi fungsi mapFreedapat memutuskan apa ayang dipakai saat mereka menggunakannya, bukan pengguna mapFree.
  • Jenis eksistensial . Sementara tipe peringkat yang lebih tinggi memungkinkan kita untuk berbicara tentang kuantifikasi universal, tipe eksistensial mari kita bicara tentang kuantifikasi eksistensial: gagasan bahwa hanya ada beberapa tipe yang tidak diketahui memenuhi beberapa persamaan. Ini akhirnya menjadi berguna dan untuk melanjutkan lebih lama tentang hal itu akan memakan waktu lama.
  • Tipe keluarga . Kadang-kadang mekanisme typeclass tidak nyaman karena kita tidak selalu berpikir dalam Prolog. Keluarga tipe mari kita tuliskan hubungan fungsional langsung antara tipe.
    • Keluarga tipe tertutup . Tipe keluarga secara default terbuka yang menjengkelkan karena itu berarti bahwa ketika Anda dapat memperpanjang mereka kapan saja Anda tidak dapat "membalikkan" mereka dengan harapan untuk sukses. Ini karena Anda tidak dapat membuktikan suntikan , tetapi dengan keluarga tipe tertutup Anda bisa.
  • Jenis diindeks jenis dan jenis promosi . Saya menjadi sangat eksotis pada saat ini, tetapi ini memiliki penggunaan praktis dari waktu ke waktu. Jika Anda ingin menulis jenis pegangan yang terbuka atau tertutup maka Anda dapat melakukannya dengan sangat baik. Perhatikan dalam cuplikan berikut ini yang Statemerupakan tipe aljabar yang sangat sederhana yang nilainya ditingkatkan menjadi tipe-level juga. Kemudian, selanjutnya, kita dapat berbicara tentang konstruktor tipe seperti Handlemengambil argumen pada jenis tertentu seperti State. Sangat membingungkan untuk memahami semua detail, tetapi juga sangat benar.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
    
  • Representasi tipe Runtime yang berfungsi . Java terkenal karena memiliki penghapusan tipe dan memiliki fitur hujan pada parade beberapa orang. Namun, penghapusan tipe adalah cara yang tepat, seolah-olah Anda memiliki fungsi getRepr :: a -> TypeReprmaka paling tidak Anda melanggar parametrik. Yang lebih buruk adalah bahwa jika itu adalah fungsi yang dibuat pengguna yang digunakan untuk memicu paksaan yang tidak aman saat runtime ... maka Anda memiliki masalah keamanan yang besar . TypeableSistem Haskell memungkinkan pembuatan brankas coerce :: (Typeable a, Typeable b) => a -> Maybe b. Sistem ini bergantung pada Typeablediimplementasikan dalam kompiler (dan bukan userland) dan juga tidak dapat diberikan semantik bagus tanpa mekanisme typeclass Haskell dan hukum itu dijamin untuk mengikuti.

Lebih dari ini, nilai sistem tipe Haskell juga berhubungan dengan bagaimana tipe menggambarkan bahasa. Berikut adalah beberapa fitur Haskell yang mendorong nilai melalui sistem tipe.

  • Kemurnian . Haskell tidak memungkinkan efek samping untuk definisi "efek samping" yang sangat, sangat, sangat luas. Ini memaksa Anda untuk memasukkan lebih banyak informasi ke dalam tipe karena tipe mengatur input dan output dan tanpa efek samping semuanya harus diperhitungkan dalam input dan output.
    • IO . Selanjutnya, Haskell membutuhkan cara untuk berbicara tentang efek samping — karena setiap program nyata harus menyertakan beberapa — sehingga kombinasi antara jenis kacamata, jenis yang lebih tinggi, dan jenis abstrak memunculkan gagasan untuk menggunakan jenis khusus, jenis super khusus yang dipanggil IO auntuk mewakili perhitungan efek samping yang menghasilkan nilai tipe a. Ini adalah dasar dari sistem efek yang sangat bagus yang tertanam di dalam bahasa murni.
  • Kurangnyanull . Semua orang tahu itu nulladalah kesalahan miliaran dolar dari bahasa pemrograman modern. Tipe aljabar, khususnya kemampuan untuk hanya menambahkan status "tidak ada" ke tipe yang Anda miliki dengan mengubah suatu tipe Amenjadi tipe Maybe A, sepenuhnya mengurangi masalah null.
  • Rekursi polimorfik . Ini memungkinkan Anda menentukan fungsi rekursif yang menggeneralisasi variabel tipe meskipun menggunakannya pada tipe yang berbeda di setiap panggilan rekursif dalam generalisasi mereka sendiri. Ini sulit untuk dibicarakan, tetapi sangat berguna untuk berbicara tentang tipe bersarang. Lihat kembali ke Bt ajenis dari sebelumnya dan mencoba untuk menulis fungsi untuk menghitung ukurannya: size :: Bt a -> Int. Ini akan terlihat seperti size (Here a) = 1dan size (There bt) = 2 * size bt. Secara operasional itu tidak terlalu rumit, tetapi perhatikan bahwa panggilan rekursif ke sizedalam persamaan terakhir terjadi pada tipe yang berbeda , namun definisi keseluruhan memiliki tipe umum yang bagus size :: Bt a -> Int. Perhatikan bahwa ini adalah fitur yang memecah total inferensi, tetapi jika Anda memberikan tanda tangan jenis maka Haskell akan mengizinkannya.

Saya bisa terus berjalan, tetapi daftar ini harus membuat Anda memulai, dan kemudian beberapa.

J. Abrahamson
sumber
7
Null bukan "kesalahan" miliar dolar. Ada kasus di mana tidak mungkin untuk memverifikasi secara statis bahwa pointer tidak akan ditereferensi sebelum ada yang berarti mungkin ada ; memiliki jebakan dereference yang dicoba dalam kasus seperti itu seringkali lebih baik daripada mengharuskan penunjuk pada awalnya mengidentifikasi objek yang tidak berarti. Saya pikir kesalahan terbesar yang berhubungan dengan nol adalah memiliki implementasi yang, diberikan char *p = NULL;, akan menjebak *p=1234, tetapi tidak akan menjebak char *q = p+5678;atau*q = 1234;
supercat
37
Itu hanya dikutip langsung dari Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Meskipun saya yakin ada saat-saat ketika nulldiperlukan dalam aritmatika pointer, saya malah mengartikannya untuk mengatakan bahwa pointer aritmatika adalah tempat yang buruk untuk meng-host semantik bahasa Anda bukan karena nol tidak masih merupakan kesalahan.
J. Abrahamson
18
@ supercat, Anda memang dapat menulis bahasa tanpa null. Apakah mengizinkannya atau tidak adalah pilihan.
Paul Draper
6
@supercat - Masalah itu juga ada di Haskell, tetapi dalam bentuk yang berbeda. Haskell biasanya malas dan tidak berubah, dan karena itu memungkinkan Anda untuk menulis p = undefinedselama ptidak dievaluasi. Lebih berguna, Anda dapat memasukkan undefinedsemacam referensi yang bisa berubah, sekali lagi selama Anda tidak mengevaluasinya. Tantangan yang lebih serius adalah dengan perhitungan malas yang mungkin tidak berakhir, yang tentu saja tidak dapat diputuskan. Perbedaan utama adalah bahwa ini semua kesalahan pemrograman yang jelas , dan tidak pernah digunakan untuk mengekspresikan logika biasa.
Christian Conkle
6
@supercat Haskell tidak memiliki semantik referensi sepenuhnya (ini adalah gagasan transparansi referensial yang mensyaratkan bahwa semuanya dilestarikan dengan mengganti referensi dengan referensi mereka). Jadi, saya pikir pertanyaan Anda salah.
J. Abrahamson
78
  • Inferensi tipe penuh. Anda benar-benar dapat menggunakan tipe kompleks di mana-mana tanpa merasa seperti, "Sial, semua yang pernah saya lakukan adalah menulis tanda tangan tipe."
  • Tipe sepenuhnya aljabar , yang membuatnya sangat mudah untuk mengekspresikan beberapa ide kompleks.
  • Haskell memiliki kelas tipe, yang merupakan semacam antarmuka, kecuali Anda tidak harus meletakkan semua implementasi untuk satu jenis di tempat yang sama. Anda bisa membuat implementasi dari kelas tipe Anda sendiri untuk tipe pihak ketiga yang ada, tanpa memerlukan akses ke sumbernya.
  • Fungsi tingkat tinggi dan rekursif memiliki kecenderungan untuk menempatkan lebih banyak fungsi ke bidang pemeriksa tipe. Ambil filter , misalnya. Dalam bahasa imperatif, Anda dapat menulis forloop untuk mengimplementasikan fungsi yang sama, tetapi Anda tidak akan memiliki jaminan tipe statis yang sama, karena forloop tidak memiliki konsep tipe pengembalian.
  • Kurangnya subtipe sangat menyederhanakan polimorfisme parametrik.
  • Jenis yang lebih tinggi (jenis jenis) relatif mudah ditentukan dan digunakan di Haskell, yang memungkinkan Anda membuat abstraksi di sekitar jenis yang benar-benar tidak terduga di Jawa.
Karl Bielefeldt
sumber
7
Jawaban yang bagus - dapatkah Anda memberi saya contoh sederhana dari jenis yang lebih tinggi, berpikir yang akan membantu saya memahami mengapa tidak mungkin dilakukan di java.
phatmanace
3
Ada beberapa contoh bagus di sini .
Karl Bielefeldt
3
Pencocokan pola juga sangat penting, artinya Anda dapat menggunakan jenis objek untuk membuat keputusan dengan sangat mudah.
Benjamin Gruenbaum
2
@BenjaminGruenbaum Saya rasa saya tidak akan menyebut fitur sistem tipe itu.
Doval
3
Sementara ADT dan HKTs jelas merupakan bagian dari jawaban, saya ragu ada orang yang menanyakan pertanyaan ini akan tahu mengapa mereka berguna, saya menyarankan bahwa kedua bagian perlu diperluas untuk menjelaskan hal ini
jk.
62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

Di Haskell: integer, integer yang mungkin nol, integer yang nilainya berasal dari dunia luar, dan integer yang mungkin berupa string, semuanya adalah tipe yang berbeda - dan kompiler akan menegakkan ini . Anda tidak dapat mengkompilasi program Haskell yang gagal menghargai perbedaan-perbedaan ini.

(Namun, Anda dapat menghilangkan deklarasi tipe. Dalam kebanyakan kasus, kompiler dapat menentukan tipe paling umum untuk variabel Anda yang akan menghasilkan kompilasi yang berhasil. Bukankah itu rapi?)

WolfeFan
sumber
11
+1 sementara jawaban ini tidak lengkap saya pikir itu jauh lebih baik bernada di tingkat pertanyaan
jk.
1
+1 Meskipun itu akan membantu menjelaskan bahwa bahasa lain juga memiliki Maybe(misalnya Java Optionaldan Scala Option), tetapi dalam bahasa-bahasa itu solusi setengah matang, karena Anda selalu dapat menetapkan nullke variabel jenis itu, dan membiarkan program Anda meledak saat dijalankan- waktu. Ini tidak dapat terjadi dengan Haskell [1], karena tidak ada nilai nol , jadi Anda tidak bisa menipu. ([1]: sebenarnya, Anda dapat menghasilkan kesalahan yang mirip dengan NullPointerException menggunakan fungsi parsial seperti fromJustketika Anda memiliki Nothing, tetapi fungsi-fungsi itu mungkin disukai).
Andres F.
2
"integer yang nilainya berasal dari dunia luar" - tidak IO Integerakan lebih dekat dengan 'subprogram yang, ketika dijalankan, memberikan integer'? As a) dalam main = c >> cnilai yang dikembalikan oleh pertama cmungkin berbeda kemudian oleh kedua csementara aakan memiliki nilai yang sama terlepas dari posisinya (selama kita berada dalam ruang lingkup tunggal) b) ada jenis yang menunjukkan nilai dari dunia luar untuk menegakkan sanitasinya (Yaitu untuk tidak menempatkan mereka secara langsung tetapi periksa dulu apakah input dari pengguna sudah benar / tidak berbahaya).
Maciej Piechotka
4
Maciej, itu akan lebih akurat. Saya berusaha keras untuk kesederhanaan.
WolfeFan
30

Banyak orang telah mendaftar hal-hal baik tentang Haskell. Tetapi dalam menjawab pertanyaan spesifik Anda "mengapa sistem tipe membuat program lebih benar?", Saya menduga jawabannya adalah "parametrik polimorfisme".

Pertimbangkan fungsi Haskell berikut:

foobar :: x -> y -> y

Ada secara harfiah hanya satu cara yang mungkin untuk melaksanakan fungsi ini. Hanya dengan jenis tanda tangan, saya dapat mengetahui dengan tepat apa fungsi ini, karena hanya ada satu hal yang dapat dilakukan. [OK, tidak cukup, tapi hampir!]

Berhentilah dan pikirkan hal itu sejenak. Itu sebenarnya masalah besar! Itu berarti jika saya menulis fungsi dengan tanda tangan ini, sebenarnya tidak mungkin untuk melakukan fungsi selain apa yang saya maksudkan. (Jenis tanda tangan itu sendiri masih bisa salah, tentu saja. Tidak ada bahasa pemrograman yang akan mencegah semua bug.)

Pertimbangkan fungsi ini:

fubar :: Int -> (x -> y) -> y

Fungsi ini tidak mungkin . Anda benar-benar tidak dapat mengimplementasikan fungsi ini. Saya bisa mengatakan itu hanya dari jenis tanda tangan.

Seperti yang Anda lihat, tanda tangan tipe Haskell memberi tahu Anda banyak sekali!


Bandingkan dengan C #. (Maaf, Java saya agak berkarat.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Ada beberapa hal yang bisa dilakukan metode ini:

  • Kembali in2sebagai hasilnya.
  • Ulangi selamanya, dan tidak pernah mengembalikan apa pun.
  • Melempar pengecualian, dan tidak pernah mengembalikan apa pun.

Sebenarnya, Haskell memiliki tiga opsi ini juga. Tetapi C # juga memberi Anda opsi tambahan:

  • Kembali nol. (Haskell tidak memiliki null.)
  • Ubah in2sebelum mengembalikannya. (Haskell tidak memiliki modifikasi di tempat.)
  • Gunakan refleksi. (Haskell tidak memiliki refleksi.)
  • Lakukan beberapa tindakan I / O sebelum mengembalikan hasil. (Haskell tidak akan membiarkan Anda melakukan I / O kecuali Anda menyatakan bahwa Anda melakukan I / O di sini.)

Refleksi adalah palu besar; menggunakan refleksi, saya dapat membuat TYobjek baru dari udara tipis, dan mengembalikannya! Saya dapat memeriksa kedua objek, dan melakukan berbagai tindakan tergantung pada apa yang saya temukan. Saya dapat melakukan modifikasi sembarang untuk kedua objek yang dilewatkan.

I / O adalah palu besar yang serupa. Kode dapat menampilkan pesan kepada pengguna, atau membuka koneksi database, atau memformat ulang harddisk Anda, atau apa pun, sungguh.


Fungsi Haskell foobar, sebaliknya, hanya dapat mengambil beberapa data dan mengembalikan data itu, tidak berubah. Itu tidak bisa "melihat" data, karena tipenya tidak diketahui pada waktu kompilasi. Itu tidak dapat membuat data baru, karena ... yah, bagaimana Anda membuat data dari jenis apa pun yang mungkin? Anda perlu refleksi untuk itu. Itu tidak dapat melakukan I / O, karena tanda tangan jenis tidak menyatakan bahwa I / O sedang dilakukan. Jadi ia tidak dapat berinteraksi dengan sistem file atau jaringan, atau bahkan menjalankan utas dalam program yang sama! (Yaitu, dijamin 100% aman.)

Seperti yang Anda lihat, dengan tidak membiarkan Anda melakukan banyak hal, Haskell memungkinkan Anda untuk membuat jaminan yang sangat kuat tentang apa yang sebenarnya kode Anda lakukan. Begitu ketat, pada kenyataannya, bahwa (untuk kode yang benar-benar polimorfik) biasanya hanya ada satu cara yang memungkinkan potongan-potongan tersebut dapat saling menyatu.

(Untuk lebih jelasnya: Masih mungkin untuk menulis fungsi Haskell di mana tanda tangan jenis tidak memberi tahu Anda banyak. Int -> IntBisa saja tentang apa pun. Tetapi meskipun demikian, kita tahu bahwa input yang sama akan selalu menghasilkan output yang sama dengan kepastian 100%. Java bahkan tidak menjamin itu!)

Matematika Matematika
sumber
4
+1 Jawaban bagus! Ini sangat kuat dan sering kurang dihargai oleh pendatang baru di Haskell. Omong-omong, fungsi "mustahil" yang lebih sederhana adalah fubar :: a -> b, bukan? (Ya, saya sadar unsafeCoerce. Saya berasumsi kita tidak membicarakan apa pun dengan nama "tidak aman", dan pendatang baru juga tidak perlu khawatir!: D)
Andres F.
Ada banyak tanda tangan jenis sederhana yang tidak bisa Anda tulis, ya. Misalnya, foobar :: xcukup tidak dapat diterapkan ...
MathematicalOrchid
Sebenarnya, Anda tidak dapat membuat kode murni tidak aman, tetapi Anda masih bisa membuatnya multi-utas. Pilihan Anda adalah "sebelum Anda mengevaluasi ini, mengevaluasi ini", "ketika Anda mengevaluasi ini, Anda mungkin ingin mengevaluasi ini di utas terpisah juga", dan "ketika Anda mengevaluasi ini, Anda mungkin ingin mengevaluasi ini juga, mungkin di utas terpisah ". Standarnya adalah "lakukan sesuka Anda", yang pada dasarnya berarti "mengevaluasi selambat mungkin".
John Dvorak
Lebih prosaically Anda dapat memanggil metode instance pada in1 atau in2 yang memiliki efek samping. Atau Anda dapat mengubah keadaan global (yang, memang, dimodelkan sebagai tindakan IO di Haskell, tetapi mungkin bukan apa yang kebanyakan orang pikirkan sebagai IO).
Doug McClean
2
@isomorphismes Tipe x -> y -> yini dapat diterapkan dengan sempurna. Jenisnya (x -> y) -> ybukan. Jenis ini x -> y -> ymengambil dua input, dan mengembalikan yang kedua. Jenis ini (x -> y) -> ymengambil fungsi yang beroperasi pada x, dan entah bagaimana harus membuat ykeluar dari itu ...
MathematicalOrchid
17

Pertanyaan SO terkait .

Saya berasumsi Anda dapat melakukan hal yang sama di haskell (mis. Hal-hal menjadi string / int yang harus benar-benar tipe data kelas satu)

Tidak, Anda benar-benar tidak bisa - setidaknya tidak dengan cara yang sama seperti yang dilakukan Java. Di Jawa, hal semacam ini terjadi:

String x = (String)someNonString;

dan Java akan dengan senang hati mencoba menggunakan non-String Anda sebagai String. Haskell tidak mengizinkan hal semacam ini, menghilangkan seluruh kelas kesalahan runtime.

nulladalah bagian dari sistem tipe (as Nothing) sehingga perlu secara eksplisit diminta dan ditangani, menghilangkan seluruh kelas kesalahan runtime lainnya.

Ada banyak manfaat halus lainnya juga - terutama di sekitar kelas reuse dan type - yang saya tidak memiliki keahlian untuk cukup tahu untuk berkomunikasi.

Namun, sebagian besar karena sistem tipe Haskell memungkinkan banyak ekspresi. Anda dapat melakukan banyak hal dengan hanya beberapa aturan. Pertimbangkan pohon Haskell yang selalu ada:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Anda telah mendefinisikan seluruh pohon biner umum (dan dua konstruktor data) dalam satu baris kode yang cukup mudah dibaca. Semua hanya menggunakan beberapa aturan (memiliki jenis penjumlahan dan jenis produk ). Itu adalah 3-4 kode file dan kelas di Jawa.

Terutama di antara mereka yang cenderung memuja sistem tipe, keringkasan / keanggunan semacam ini sangat dihargai.

Telastyn
sumber
Saya hanya mengerti NullPointerExceptions dari jawaban Anda. Bisakah Anda memasukkan lebih banyak contoh?
Jesvin Jose
2
Belum tentu benar, JLS §5.5.1 : Jika T adalah tipe kelas, maka | S | <: | T |, atau | T | <: | S |. Kalau tidak, kesalahan waktu kompilasi terjadi. Jadi kompiler tidak akan memungkinkan Anda untuk menggunakan tipe yang tidak dapat dikonversi - jelas ada beberapa cara untuk mengatasinya.
Boris the Spider
Menurut pendapat saya cara paling sederhana untuk menempatkan keuntungan dari kelas tipe adalah bahwa mereka seperti interfaces yang dapat ditambahkan setelah-fakta, dan mereka tidak "lupa" jenis yang mengimplementasikannya. Yaitu, Anda dapat memastikan bahwa dua argumen untuk suatu fungsi memiliki tipe yang sama, tidak seperti dengan interfaces di mana dua List<String>s mungkin memiliki implementasi yang berbeda. Anda secara teknis dapat melakukan sesuatu yang sangat mirip di Jawa dengan menambahkan parameter tipe ke setiap antarmuka, tetapi 99% dari antarmuka yang ada tidak melakukannya dan Anda akan membingungkan teman-teman Anda.
Doval
2
@BoristheSpider Benar, tetapi membuat pengecualian hampir selalu melibatkan downcasting dari superclass ke subclass atau dari antarmuka ke kelas, dan itu tidak biasa bagi superclass Object.
Doval
2
Saya pikir titik dalam pertanyaan tentang string tidak berkaitan dengan casting dan runtime type error, tetapi fakta bahwa jika Anda tidak ingin menggunakan jenis, Java tidak akan membuat Anda - seperti pada, sebenarnya menyimpan data Anda dalam serial formulir, menyalahgunakan string sebagai jenis ad-hoc any. Haskell juga tidak akan menghentikanmu melakukan ini, karena ... yah, ia memiliki string. Haskell dapat memberi Anda alat, itu tidak bisa secara paksa menghentikan Anda dari melakukan hal-hal bodoh jika Anda bersikeras pada Greenspunning cukup dari seorang juru bahasa untuk menemukan kembali nulldalam konteks bersarang. Tidak ada bahasa yang bisa.
Leushenko
0

Salah satu 'meme' yang sering dibicarakan orang-orang, adalah keseluruhan "jika dikompilasi, itu akan berfungsi *" - yang saya pikir terkait dengan kekuatan sistem tipe.

Ini sebagian besar benar dengan program kecil. Haskell mencegah Anda membuat kesalahan yang mudah dalam bahasa lain (misalnya membandingkan Int32dan Word32dan sesuatu meledak), tetapi itu tidak mencegah Anda dari semua kesalahan.

Haskell sebenarnya membuat refactoring jauh lebih mudah. Jika program Anda sebelumnya benar dan itu ketik pemeriksaan, ada kemungkinan adil itu akan tetap benar setelah perbaikan kecil.

Saya mencoba memahami mengapa Haskell lebih baik daripada bahasa yang diketik secara statis dalam hal ini.

Jenis-jenis di Haskell cukup ringan, sehingga mudah untuk mendeklarasikan jenis-jenis baru. Ini berbeda dengan bahasa seperti Rust, di mana semuanya hanya sedikit lebih rumit.

Asumsi saya adalah bahwa inilah yang orang maksud dengan sistem tipe yang kuat, tetapi tidak jelas bagi saya mengapa Haskell lebih baik.

Haskell memiliki banyak fitur di luar jumlah sederhana dan jenis produk; ia memiliki tipe yang dikuantifikasi secara universal (misalnya id :: a -> a) juga. Anda juga dapat membuat tipe rekaman yang berisi fungsi, yang sangat berbeda dari bahasa seperti Java atau Rust.

GHC juga dapat menurunkan beberapa instance berdasarkan tipe saja, dan sejak munculnya generik, Anda dapat menulis fungsi yang generik di antara tipe. Ini cukup nyaman, dan lebih lancar daripada yang sama di Jawa.

Perbedaan lainnya adalah bahwa Haskell cenderung memiliki kesalahan tipe yang relatif baik (setidaknya pada saat penulisan). Jenis inferensi Haskell canggih, dan sangat jarang Anda perlu memberikan anotasi jenis untuk mendapatkan sesuatu untuk dikompilasi. Ini berbeda dengan Rust, di mana inferensi tipe kadang-kadang dapat membutuhkan anotasi bahkan ketika kompiler pada prinsipnya dapat menyimpulkan tipe.

Akhirnya, Haskell memiliki kacamata ketik, di antaranya adalah monad yang terkenal. Monads merupakan cara yang sangat baik untuk menangani kesalahan; mereka pada dasarnya memberi Anda hampir semua kenyamanan nulltanpa debugging mengerikan dan tanpa memberikan keamanan jenis apa pun. Jadi kemampuan untuk menulis fungsi pada tipe-tipe ini sebenarnya sangat penting ketika mendorong kita untuk menggunakannya!

Dengan kata lain, Anda dapat menulis Java baik atau buruk, saya menganggap Anda dapat melakukan hal yang sama di Haskell

Itu mungkin benar, tetapi tidak ada poin penting: titik di mana Anda mulai menembak diri sendiri di kaki Haskell lebih jauh dari titik ketika Anda mulai menembak diri sendiri di kaki di Jawa.


sumber