Mengapa tidak diketik secara dependen?

161

Saya telah melihat beberapa sumber menggemakan pendapat bahwa "Haskell secara bertahap menjadi bahasa yang diketik secara dependen". Implikasinya tampaknya bahwa dengan semakin banyak ekstensi bahasa, Haskell melayang ke arah yang umum, tetapi belum ada di sana.

Pada dasarnya ada dua hal yang ingin saya ketahui. Yang pertama adalah, cukup sederhana, apa "yang bahasa dependen-mengetik" benar-benar berarti ? (Semoga tanpa terlalu teknis tentang hal itu.)

Pertanyaan kedua adalah ... apa kekurangannya? Maksudku, orang tahu kita menuju ke sana, jadi pasti ada keuntungannya. Namun, kita belum sampai di sana, jadi pasti ada beberapa hal buruk yang membuat orang berhenti. Saya mendapat kesan bahwa masalahnya adalah peningkatan kompleksitas yang curam. Tapi, tidak benar-benar memahami apa yang dimaksud dengan mengetik, saya tidak tahu pasti.

Apa yang saya lakukan tahu adalah bahwa setiap kali saya mulai membaca tentang bahasa pemrograman dependen-diketik, teks sama sekali tidak bisa dimengerti ... Agaknya itulah masalahnya. (?)

Matematika Matematika
sumber
10
Sederhananya, Anda dapat menulis jenis yang tergantung pada istilah (perhitungan). Ini cukup untuk menentukan jenis tentang setiap aspek program Anda, dan oleh karena itu berarti sistem jenis mampu spesifikasi program lengkap. Masalahnya adalah karena jenis tergantung pada perhitungan, pemeriksaan jenis jauh lebih sulit dilakukan (secara umum tidak mungkin).
GManNickG
27
@GManNickG: Pemeriksaan tipe sangat mungkin dilakukan. Ketik inferensi adalah masalah lain, tetapi sekali lagi berbagai ekstensi GHC telah lama meninggalkan gagasan bahwa mungkin untuk menyimpulkan semua jenis.
CA McCann
7
Jika saya mengerti dengan benar, kekurangannya adalah melakukan pengetikan yang benar tergantung (misalnya, dengan cara yang dapat digunakan dan beralasan) adalah sulit , dan kami belum tahu seberapa baik.
badai
1
@CAMcCann: Ya, kesalahan saya.
GManNickG
4
Saya tidak berpikir ada orang yang menunjukkan satu kelemahan pragmatis besar: menulis bukti bahwa semua kode Anda benar sangat membosankan. Karena Anda tidak dapat secara otomatis melakukan inferensi jenis (sesuai dengan teorema yang membuktikan logika "hella powerful"), Anda harus menulis anotasi untuk program Anda dalam bentuk bukti. Ini jelas menjengkelkan dan sulit dilakukan setelah beberapa saat, terutama untuk sihir monadik yang lebih rumit yang biasanya dilakukan orang di Haskell. Yang paling dekat dengan kami saat ini adalah bahasa yang melakukan sebagian besar untuk kami atau memberi kami kumpulan primitif yang baik.
Kristopher Micinski

Jawaban:

21

Pengetikan dependen sebenarnya hanya penyatuan nilai dan tingkat tipe, sehingga Anda dapat menentukan nilai pada tipe (sudah dimungkinkan dengan kelas tipe dan polimorfisme parametrik di Haskell) dan Anda dapat menentukan tipe pada nilai (tidak, secara tegas, mungkin di Haskell , meskipun DataKindssangat dekat).

Sunting: Rupanya, sejak saat ini, saya salah (lihat komentar @ pigworker). Saya akan melestarikan sisanya sebagai catatan mitos yang telah saya makan. : P


Masalah dengan pindah ke pengetikan sepenuhnya tergantung, dari apa yang saya dengar, adalah bahwa itu akan mematahkan pembatasan fase antara jenis dan tingkat nilai yang memungkinkan Haskell dikompilasi ke kode mesin yang efisien dengan tipe yang terhapus. Dengan tingkat teknologi kami saat ini, bahasa yang diketik secara dependen harus melalui penerjemah di beberapa titik (baik segera, atau setelah dikompilasi ke bytecode yang diketik-tergantung atau serupa).

Ini belum tentu merupakan batasan mendasar, tetapi saya secara pribadi tidak mengetahui adanya penelitian saat ini yang terlihat menjanjikan dalam hal ini, tetapi belum membuatnya menjadi GHC. Jika ada orang lain yang tahu lebih banyak, saya akan senang dikoreksi.

Api Ptharien
sumber
46
Apa yang Anda katakan hampir seluruhnya salah. Saya tidak sepenuhnya menyalahkan Anda: itu mengulang mitos standar sebagai fakta. Bahasa Edwin Brady, Idris, melakukan penghapusan tipe (karena tidak ada perilaku run-time tergantung pada jenis) dan menghasilkan pengkodean supercombinator lambda-lifted yang cukup standar dari mana kode dihasilkan dengan menggunakan teknik mesin G stock.
Pigworker
3
Sebagai catatan, seseorang baru-baru ini mengarahkan saya ke makalah ini . Dari apa yang bisa saya katakan, itu akan membuat Haskell tipe dependen (yaitu, tipe level bahasa akan dependen-mengetik), yang sedekat yang saya bisa lihat kita dapatkan dalam waktu dekat.
Flame Ptharien,
8
Ya, kertas itu memang menunjukkan cara membuat tipe tergantung pada jenis level (dan untuk menghilangkan perbedaan tipe / jenis). Tindak lanjut yang masuk akal, sudah dalam diskusi, adalah untuk mengizinkan tipe fungsi dependen yang sebenarnya, tetapi membatasi argumen mereka pada fragmen bahasa yang dapat ada di layer nilai dan tipe (sekarang nontrivial berkat promosi tipe data). Itu akan menghilangkan kebutuhan untuk konstruksi tunggal yang saat ini membuat "berpura-pura" lebih kompleks daripada yang diinginkan. Kami semakin dekat dengan hal yang nyata.
Pigworker
13
Ada banyak pertanyaan pragmatis, perkuatan tipe dependen untuk Haskell. Setelah kita mendapatkan bentuk ruang fungsi dependen yang terbatas ini, kita masih menghadapi pertanyaan tentang bagaimana memperbesar fragmen bahasa nilai yang diizinkan pada level tipe, dan seperti apa teori persamaannya (seperti yang kita inginkan 2 + 2 untuk menjadi 4, dan semacamnya). Ada banyak masalah fiddly (mis., Bagian bawah) yang dari awal mengetik bahasa yang disain jauh dari awal.
Pigworker
2
@pigworker Apakah ada subkumpulan Haskell yang bersih yang total? Jika demikian, tidak bisakah kita hanya menggunakannya untuk "fragmen bahasa yang bisa ada di lapisan nilai dan tipe" Jika tidak, apa yang diperlukan untuk menghasilkannya?
Flame Ptharien
223

Mengetik Haskell dengan Ketergantungan, Sekarang?

Haskell, sebagian kecil, adalah bahasa yang diketik secara dependen. Ada gagasan tentang tipe data tingkat, sekarang lebih masuk akal mengetik berkat DataKinds, dan ada beberapa cara ( GADTs) untuk memberikan representasi run-time untuk data tipe tingkat. Oleh karena itu, nilai run-time stuff secara efektif muncul dalam tipe , yang artinya apa yang harus diketik dalam bahasa.

Tipe data sederhana dipromosikan ke tingkat jenis, sehingga nilai yang dikandungnya dapat digunakan dalam jenis. Karena itulah contoh pola dasar

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where
  VNil   :: Vec Z x
  VCons  :: x -> Vec n x -> Vec (S n) x

menjadi mungkin, dan dengan itu, definisi seperti

vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil         VNil         = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)

itu bagus. Perhatikan bahwa panjangnya nadalah hal yang murni statis dalam fungsi itu, memastikan bahwa vektor input dan output memiliki panjang yang sama, meskipun panjang itu tidak berperan dalam eksekusi vApply. Sebaliknya, itu jauh lebih sulit (yaitu, tidak mungkin) untuk melaksanakan fungsi yang membuat nsalinan yang diberikan x(yang akan pureke vApply's <*>)

vReplicate :: x -> Vec n x

karena sangat penting untuk mengetahui berapa banyak salinan yang harus dibuat pada saat run-time. Masukkan lajang.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

Untuk semua jenis yang dapat dipromosikan, kita dapat membangun keluarga tunggal, diindeks dari jenis yang dipromosikan, dihuni oleh duplikat run-time dari nilainya. Natty nadalah jenis salinan run-time dari tipe-level n :: Nat. Kita sekarang bisa menulis

vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy     x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)

Jadi, di sana Anda memiliki nilai level-tipe yang dipasangkan ke nilai run-time: memeriksa salinan run-time memurnikan pengetahuan statis tentang nilai level-tipe. Meskipun istilah dan jenisnya terpisah, kita dapat bekerja dengan cara yang diketik secara dependen dengan menggunakan konstruksi tunggal sebagai semacam resin epoksi, menciptakan ikatan di antara fase-fase tersebut. Itu jauh dari memungkinkan ekspresi run-time sewenang-wenang dalam jenis, tapi itu bukan apa-apa.

Apa itu Nasty? Apa yang hilang?

Mari kita beri sedikit tekanan pada teknologi ini dan lihat apa yang mulai goyah. Kita mungkin mendapat gagasan bahwa lajang harus dikelola sedikit lebih implisit

class Nattily (n :: Nat) where
  natty :: Natty n
instance Nattily Z where
  natty = Zy
instance Nattily n => Nattily (S n) where
  natty = Sy natty

memungkinkan kita untuk menulis, katakan,

instance Nattily n => Applicative (Vec n) where
  pure = vReplicate natty
  (<*>) = vApply

Itu bekerja, tetapi sekarang berarti bahwa Nattipe asli kami telah menghasilkan tiga salinan: sejenis, keluarga tunggal dan kelas tunggal. Kami memiliki proses yang agak kikuk untuk bertukar Natty nnilai dan Nattily nkamus eksplisit . Selain itu, Nattytidak Nat: kita memiliki semacam ketergantungan pada nilai run-time, tetapi tidak pada jenis yang pertama kali kita pikirkan. Tidak ada bahasa yang diketik sepenuhnya tergantung membuat jenis dependen ini rumit!

Sementara itu, meski Natbisa dipromosikan, Vectidak bisa. Anda tidak dapat mengindeks berdasarkan tipe yang diindeks. Penuh pada bahasa yang diketik secara dependen tidak memberlakukan batasan seperti itu, dan dalam karier saya sebagai pamer yang diketik secara dependen, saya telah belajar untuk menyertakan contoh pengindeksan dua lapis dalam pembicaraan saya, hanya untuk mengajar orang-orang yang telah membuat pengindeksan satu lapis sulit-tetapi-mungkin untuk tidak mengharapkan saya melipat seperti rumah kartu. Apa masalahnya? Persamaan. GADT bekerja dengan menerjemahkan kendala yang Anda capai secara implisit ketika Anda memberikan konstruktor tipe pengembalian spesifik ke dalam tuntutan persamaan eksplisit. Seperti ini.

data Vec (n :: Nat) (x :: *)
  = n ~ Z => VNil
  | forall m. n ~ S m => VCons x (Vec m x)

Di masing-masing dari dua persamaan kami, kedua belah pihak memiliki jenis Nat.

Sekarang coba terjemahan yang sama untuk sesuatu yang diindeks oleh vektor.

data InVec :: x -> Vec n x -> * where
  Here :: InVec z (VCons z zs)
  After :: InVec z ys -> InVec z (VCons y ys)

menjadi

data InVec (a :: x) (as :: Vec n x)
  = forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
  | forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)

dan sekarang kita membentuk kendala persamaan antara as :: Vec n xdan di VCons z zs :: Vec (S m) xmana kedua belah pihak memiliki jenis yang berbeda secara sintaksis (tetapi terbukti sama). Inti GHC saat ini tidak dilengkapi untuk konsep seperti itu!

Apa lagi yang hilang? Yah, sebagian besar Haskell hilang dari level tipenya. Bahasa istilah yang dapat Anda promosikan benar-benar memiliki variabel dan konstruktor non-GADT. Setelah Anda memilikinya, type familymesinnya memungkinkan Anda untuk menulis program level-type: beberapa di antaranya mungkin sangat mirip dengan fungsi yang Anda pertimbangkan untuk menulis pada level term (misalnya, melengkapi Natdengan penambahan, sehingga Anda dapat memberikan tipe yang bagus untuk ditambahkan Vec) , tapi itu hanya kebetulan!

Hal lain yang hilang, dalam praktiknya, adalah perpustakaan yang memanfaatkan kemampuan baru kami untuk mengindeks tipe berdasarkan nilai. Apa yang terjadi Functor dan Monadmenjadi di dunia baru yang berani ini? Saya sedang memikirkannya, tetapi masih banyak yang harus dilakukan.

Menjalankan Program Level-Type

Haskell, seperti kebanyakan bahasa pemrograman yang diketik secara dependen, memiliki dua semantik operasional. Ada cara sistem run-time menjalankan program (hanya ekspresi tertutup, setelah penghapusan tipe, sangat dioptimalkan) dan kemudian ada cara typechecker menjalankan program (keluarga tipe Anda, "jenis kelas Prolog", dengan ekspresi terbuka). Untuk Haskell, Anda biasanya tidak mencampur keduanya, karena program yang dijalankan dalam bahasa yang berbeda. Bahasa ketergantungan diketik memiliki terpisah run-time dan model eksekusi statis untuk sama bahasa program, tapi jangan khawatir, model run-time masih memungkinkan Anda melakukan penghapusan jenis dan, memang, bukti penghapusan: yang ini apa Coq ini ekstraksimekanisme memberi Anda; setidaknya itulah yang dikompilasi oleh Edwin Brady (walaupun Edwin menghapus nilai-nilai yang tidak perlu, juga tipe dan bukti). Perbedaan fase mungkin bukan perbedaan kategori sintaksis lagi, tapi itu hidup dan sehat.

Bahasa yang diketik dengan tergantung, sebagai total, memungkinkan juru ketik untuk menjalankan program bebas dari rasa takut akan sesuatu yang lebih buruk daripada menunggu lama. Ketika Haskell menjadi lebih sering diketik, kita menghadapi pertanyaan seperti apa model eksekusi statisnya? Salah satu pendekatan mungkin untuk membatasi eksekusi statis untuk fungsi total, yang akan memungkinkan kita untuk menjalankan kebebasan yang sama, tetapi mungkin memaksa kita untuk membuat perbedaan (setidaknya untuk kode tingkat-jenis) antara data dan codata, sehingga kita dapat menentukan apakah harus memberlakukan pemutusan hubungan kerja atau produktivitas. Tapi itu bukan satu-satunya pendekatan. Kita bebas memilih model pelaksanaan yang jauh lebih lemah yang enggan menjalankan program, dengan biaya membuat lebih sedikit persamaan yang keluar hanya dengan perhitungan. Dan sebenarnya, itulah yang sebenarnya dilakukan GHC. Aturan pengetikan untuk inti GHC tidak menyebutkan menjalankan program, tetapi hanya untuk memeriksa bukti untuk persamaan. Saat menerjemahkan ke inti, pemecah kendala GHC mencoba menjalankan program level-type Anda, menghasilkan sedikit jejak bukti keperakan bahwa ekspresi yang diberikan sama dengan bentuk normalnya. Metode pembuktian-bukti ini sedikit tidak dapat diprediksi dan tidak bisa dihindari: metode ini berjuang melawan rekursi yang tampak menakutkan, misalnya, dan itu mungkin bijaksana. Satu hal yang tidak perlu kita khawatirkan adalah pelaksanaan IO perhitungan di typechecker: ingat bahwa typechecker tidak harus memberikan launchMissilesarti yang sama dengan sistem run-time!

Budaya Hindley-Milner

Sistem tipe Hindley-Milner mencapai kebetulan yang benar-benar luar biasa dari empat perbedaan yang berbeda, dengan efek samping budaya yang disayangkan bahwa banyak orang tidak dapat melihat perbedaan antara perbedaan dan menganggap kebetulan itu tidak bisa dihindari! Apa yang saya bicarakan?

  • istilah vs tipe
  • hal-hal yang ditulis secara eksplisit vs hal-hal yang ditulis secara implisit
  • Kehadiran pada run-time vs penghapusan sebelum run-time
  • abstraksi non-dependen vs kuantifikasi dependen

Kami terbiasa menulis istilah dan meninggalkan jenis yang akan disimpulkan ... lalu dihapus. Kami terbiasa menghitung variabel tipe dengan abstraksi tipe yang sesuai dan aplikasi terjadi secara diam-diam dan statis.

Anda tidak perlu membelok terlalu jauh dari vanilla Hindley-Milner sebelum perbedaan ini tidak selaras, dan itu bukan hal yang buruk . Sebagai permulaan, kita dapat memiliki jenis yang lebih menarik jika kita bersedia menulisnya di beberapa tempat. Sementara itu, kita tidak harus menulis kamus kelas ketik ketika kita menggunakan fungsi-fungsi yang kelebihan beban, tetapi kamus-kamus itu pasti ada (atau sebaris) pada saat run-time. Dalam bahasa yang diketik secara dependen, kami berharap untuk menghapus lebih dari sekadar tipe saat run-time, tetapi (seperti dengan kelas tipe) bahwa beberapa nilai yang disimpulkan secara implisit tidak akan dihapus. Misalnya, vReplicateargumen numerik seringkali dapat disimpulkan dari jenis vektor yang diinginkan, tetapi kita masih perlu mengetahuinya pada saat run-time.

Pilihan desain bahasa mana yang harus kita tinjau karena kebetulan ini tidak lagi berlaku? Misalnya, apakah benar Haskell tidak memberikan cara untuk membuat instance forall x. tquantifier secara eksplisit? Jika typechecker tidak dapat menebak xdengan menyatukan t, kami tidak memiliki cara lain untuk mengatakan apa yang xharus terjadi.

Secara lebih luas, kita tidak bisa memperlakukan "inferensi tipe" sebagai konsep monolitik yang kita miliki semuanya atau tidak sama sekali. Sebagai permulaan, kita perlu memisahkan aspek "generalisasi" (aturan "biarkan" Milner), yang sangat bergantung pada pembatasan jenis mana yang ada untuk memastikan bahwa mesin bodoh dapat menebak satu, dari aspek "spesialisasi" (Milner's "var "rule) yang sama efektifnya dengan pemecah kendala Anda. Kita dapat berharap bahwa tipe tingkat atas akan menjadi lebih sulit untuk disimpulkan, tetapi informasi tipe internal akan tetap cukup mudah untuk disebarkan.

Langkah Selanjutnya Untuk Haskell

Kami melihat jenis dan tingkat jenis tumbuh sangat mirip (dan mereka sudah berbagi representasi internal di GHC). Kita mungkin juga menggabungkan mereka. Akan menyenangkan untuk mengambil * :: *jika kita bisa: kita kehilangan kesehatan logis sejak lama, ketika kita membiarkan bagian bawah, tetapi jenis kesehatan biasanya merupakan persyaratan yang lebih lemah. Kita harus periksa. Jika kita harus memiliki tipe, jenis, dan level yang berbeda, kita setidaknya dapat memastikan semuanya pada level tipe dan di atas selalu dapat dipromosikan. Alangkah baiknya hanya menggunakan kembali polimorfisme yang sudah kita miliki untuk tipe, daripada menciptakan kembali polimorfisme pada tingkat jenis.

Kita harus menyederhanakan dan menggeneralisasi sistem kendala saat ini dengan memungkinkan persamaan heterogen dia ~ b mana jenis adan btidak identik secara sintaksis (tetapi dapat dibuktikan sama). Ini adalah teknik lama (dalam tesis saya, abad lalu) yang membuat ketergantungan jauh lebih mudah untuk diatasi. Kami dapat mengekspresikan kendala pada ekspresi di GADT, dan dengan demikian melonggarkan pembatasan pada apa yang dapat dipromosikan.

Kita harus menghilangkan kebutuhan untuk konstruksi tunggal dengan memperkenalkan tipe fungsi dependen pi x :: s -> t,. Fungsi dengan tipe seperti itu dapat diterapkan secara eksplisit pada ekspresi tipe apa pun syang hidup di persimpangan jenis dan istilah bahasa (jadi, variabel, konstruktor, dengan lebih banyak yang akan datang nanti). Lambda dan aplikasi yang sesuai tidak akan dihapus pada saat run-time, jadi kami akan dapat menulis

vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z     x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)

tanpa mengganti Natdengan Natty. Domain pidapat berupa jenis apa pun yang dapat dipromosikan, jadi jika GADT dapat dipromosikan, kita dapat menulis sekuens kuantifier dependen (atau "teleskop" seperti yang disebut de Briuijn)

pi n :: Nat -> pi xs :: Vec n x -> ...

sejauh apa pun yang kita butuhkan.

Inti dari langkah-langkah ini adalah untuk menghilangkan kerumitan dengan bekerja secara langsung dengan alat yang lebih umum, alih-alih puas dengan alat yang lemah dan penyandian yang kikuk. Dukungan parsial saat ini membuat manfaat tipe dependen Haskell lebih mahal daripada yang seharusnya.

Terlalu keras?

Jenis ketergantungan membuat banyak orang gelisah. Mereka membuat saya gugup, tetapi saya suka gugup, atau setidaknya saya merasa sulit untuk tidak gugup. Tapi itu tidak membantu bahwa ada kabut ketidaktahuan di sekitar topik tersebut. Beberapa di antaranya karena kenyataan bahwa kita semua masih harus banyak belajar. Namun para pendukung pendekatan yang kurang radikal diketahui memicu rasa takut terhadap tipe-tipe dependen tanpa selalu memastikan fakta sepenuhnya ada pada mereka. Saya tidak akan menyebutkan nama. Ini "pemeriksaan ketik yang tidak dapat ditentukan", "Turing tidak lengkap", "tidak ada perbedaan fasa", "tidak ada penghapusan tipe", "bukti di mana-mana", dll, mitos tetap ada, meskipun itu adalah sampah.

Ini tentu bukan kasus bahwa program yang diketik secara dependen harus selalu terbukti benar. Seseorang dapat meningkatkan kebersihan dasar dari program seseorang, memberlakukan invarian tambahan dalam jenis tanpa pergi ke spesifikasi lengkap. Langkah-langkah kecil ke arah ini cukup sering menghasilkan jaminan yang lebih kuat dengan sedikit atau tanpa kewajiban bukti tambahan. Tidak benar bahwa program-program yang diketik secara tergantung pasti penuh dengan bukti, memang saya biasanya mengambil keberadaan bukti dalam kode saya sebagai isyarat untuk mempertanyakan definisi saya .

Karena, seperti halnya dengan peningkatan dalam artikulasi, kita menjadi bebas untuk mengatakan hal-hal baru yang buruk serta adil. Misalnya, ada banyak cara payah untuk mendefinisikan pohon pencarian biner, tetapi itu tidak berarti tidak ada cara yang baik . Sangat penting untuk tidak menganggap bahwa pengalaman buruk tidak bisa lebih baik, bahkan jika ego menolaknya. Desain definisi dependen adalah keterampilan baru yang membutuhkan pembelajaran, dan menjadi programmer Haskell tidak secara otomatis membuat Anda menjadi seorang ahli! Dan bahkan jika beberapa program curang, mengapa Anda menolak orang lain kebebasan untuk adil?

Kenapa Masih Mengganggu Haskell?

Saya sangat menikmati tipe dependen, tetapi sebagian besar proyek peretasan saya masih di Haskell. Mengapa? Haskell memiliki kelas tipe. Haskell memiliki perpustakaan yang bermanfaat. Haskell memiliki perawatan pemrograman yang bisa diterapkan (walaupun jauh dari ideal) dengan efek. Haskell memiliki kompiler kekuatan industri. Bahasa yang diketik secara dependen berada pada tahap yang jauh lebih awal dalam menumbuhkan komunitas dan infrastruktur, tetapi kita akan sampai di sana, dengan perubahan generasi nyata dalam apa yang mungkin, misalnya, dengan cara pemrograman dan generik tipe data. Tetapi Anda hanya perlu melihat-lihat apa yang dilakukan orang sebagai hasil dari langkah-langkah Haskell menuju tipe-tipe dependen untuk melihat bahwa ada banyak manfaat yang bisa diperoleh dengan mendorong generasi bahasa sekarang ke depan juga.

pekerja pigmen
sumber
6
Saya benar-benar tidak peduli tentang hal-hal DataKinds. Terutama karena aku ingin melakukan sesuatu seperti ini: fmap read getLine >>= \n -> vReplicate n 0. Seperti yang Anda perhatikan, Nattyada cara untuk menjauh dari ini. Selain itu, vReplicate harus diterjemahkan ke array memori yang sebenarnya, sesuatu seperti newtype SVector n x = SVector (Data.Vector.Vector x), di mana nmemiliki jenis Nat(atau serupa). Mungkin titik demonstrasi lain untuk "pamer ketergantungan-mengetik?"
John L
7
Bisakah Anda mengatakan apa yang ada dalam pikiran Anda untuk perawatan pemrograman yang ideal dengan efek?
Steven Shaw
6
Terima kasih atas artikelnya yang bagus. Saya ingin melihat beberapa contoh kode yang diketik secara dependen di mana beberapa data berasal dari luar program (misalnya membaca dari file), untuk mengetahui bagaimana mempromosikan nilai-nilai ke jenis akan terlihat seperti dalam pengaturan seperti itu. Saya merasa bahwa semua contoh melibatkan vektor (diimplementasikan sebagai daftar) dengan ukuran yang diketahui secara statis.
Tibb
4
@pigworker Anda menganggap "tidak ada perbedaan fase" sebagai mitos (yang lain saya setuju adalah mitos). Tetapi Anda belum membongkar yang ini dalam makalah dan pembicaraan yang pernah saya lihat, dan sementara itu orang lain yang saya hormati memberi tahu saya "teori tipe dependen berbeda dari kompiler tipikal karena kita tidak dapat memisahkan tahap-tahap pengecekan, kompilasi, dan eksekusi. " (lihat posting terbaru Andrej nov 8 2012) Dalam pengalaman saya "berpura-pura", kadang-kadang kita setidaknya mengaburkan perbedaan fase meskipun tidak perlu menghapusnya. Bisakah Anda memperluas, jika tidak di sini lalu di tempat lain, tentang masalah ini?
sclv
4
@ sclv Pekerjaan saya belum secara khusus menargetkan mitos "tanpa perbedaan fase", tetapi yang lain miliki. Saya merekomendasikan penolakan "Fase Perbedaan dalam Kompilasi Epigram", oleh James McKinna dan Edwin Brady, sebagai tempat yang baik untuk memulai. Tetapi lihat juga pekerjaan yang jauh lebih tua tentang Program Ekstraksi dalam Coq. Evaluasi istilah terbuka yang dilakukan oleh juru ketik sepenuhnya terpisah dari eksekusi melalui ekstraksi ke ML, dan jelas bahwa ekstraksi menghapus jenis dan bukti.
pekerja babi
20

John itu kesalahpahaman umum lain tentang tipe dependen: bahwa mereka tidak berfungsi ketika data hanya tersedia saat run-time. Inilah cara Anda dapat melakukan contoh getLine:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Sunting: Hm, itu seharusnya menjadi komentar untuk jawaban pekerja tambang. Saya jelas gagal di SO.

ulfnorell
sumber
Kalimat pertama Anda agak aneh; Saya akan mengatakan titik jenis tergantung adalah bahwa mereka melakukan pekerjaan ketika data hanya tersedia di run-time. Namun, teknik gaya CPS ini tidak sama. Misalkan Anda memiliki fungsi Vec Zy -> IO String. Anda tidak dapat menggunakannya dengan withZeroes, karena jenisnya Zytidak dapat disatukan dengan forall n. Mungkin Anda bisa mengatasinya untuk satu atau dua kasus khusus, tetapi cepat hilang kendali.
John L
Kunci ketika mengambil nilai yang diketik sederhana (seperti String dari getLine) dan mengubahnya menjadi sesuatu dengan tipe yang lebih kuat (seperti Natty n di atas) adalah Anda harus meyakinkan pemeriksa tipe bahwa Anda sedang melakukan pemeriksaan dinamis yang diperlukan. Dalam contoh Anda, Anda membaca angka acak sehingga forall nmasuk akal. Pembatasan yang lebih tepat dapat diterapkan dengan cara yang sama. Apakah Anda memiliki contoh yang lebih baik daripada Vec Zy(program masih perlu menangani pengguna memasukkan 5 daripada 0)?
ulfnorell
1
Apa yang ingin saya katakan dengan kalimat pertama adalah bahwa saya kadang-kadang bertemu dengan orang-orang yang percaya bahwa Anda tidak dapat menggunakan tipe dependen jika Anda mendapatkan data Anda dengan berinteraksi dengan dunia luar. Maksud saya adalah bahwa satu-satunya hal yang harus Anda lakukan adalah menulis parser yang diketik secara dependen, yang biasanya lurus ke depan.
ulfnorell
1
ulfnorell: Maaf, saya tidak jelas. Misalkan Anda memiliki satu fungsi yang akan bekerja dengan Vec Zy -> IO Stringdan yang lain untuk Vec n -> IO String, dan Anda ingin menggunakan yang pertama hanya jika jenisnya cocok. Ya itu mungkin, tetapi mekanisme untuk mengaktifkannya kikuk. Dan ini adalah logika yang sangat sederhana; jika Anda memiliki logika yang lebih kompleks, itu lebih buruk. Juga, Anda mungkin perlu menulis ulang banyak kode dalam CPS. Dan Anda masih belum memiliki ekspresi tipe-level yang bergantung pada istilah pada level nilai
John L
Ah, aku mengerti apa yang kamu katakan. Ini adalah tujuan Natty, seperti di vReplicate di mana kita melakukan hal-hal yang berbeda tergantung pada n. Memang ini bisa sedikit kikuk. Sebuah alternatif untuk gaya CPS adalah untuk bekerja dengan mengemasi existentials: zeroes :: IO (Some (Flip Vec Int)).
ulfnorell
19

Pigworker memberikan diskusi yang bagus tentang mengapa kita harus menuju pada tipe-tipe dependen: (a) mereka mengagumkan; (b) mereka sebenarnya akan menyederhanakan banyak hal yang sudah dilakukan Haskell.

Adapun "mengapa tidak?" pertanyaan, ada beberapa poin yang saya pikir. Poin pertama adalah bahwa sementara gagasan dasar di balik tipe dependen mudah (memungkinkan tipe bergantung pada nilai-nilai), konsekuensi dari gagasan dasar tersebut halus dan mendalam. Sebagai contoh, perbedaan antara nilai dan tipe masih hidup dan baik; tetapi mendiskusikan perbedaan di antara mereka menjadi jauhlebih bernuansa daripada di Hindley Anda - Milner atau Sistem F. Untuk beberapa hal ini disebabkan oleh fakta bahwa tipe dependen secara fundamental sulit (misalnya, logika tingkat pertama tidak dapat diputuskan). Tapi saya pikir masalah yang lebih besar adalah kita tidak memiliki kosakata yang baik untuk menangkap dan menjelaskan apa yang terjadi. Semakin banyak orang belajar tentang tipe dependen, kami akan mengembangkan kosakata yang lebih baik dan hal-hal akan menjadi lebih mudah untuk dipahami, bahkan jika masalah yang mendasarinya masih sulit.

Poin kedua berkaitan dengan fakta bahwa Haskell adalah tumbuhmenuju tipe dependen. Karena kami membuat kemajuan bertahap ke arah tujuan itu, tetapi tanpa benar-benar membuatnya di sana, kami terjebak dengan bahasa yang memiliki tambalan tambahan di atas tambalan tambahan. Hal yang sama telah terjadi dalam bahasa lain ketika ide-ide baru menjadi populer. Java tidak terbiasa memiliki polimorfisme (parametrik); dan ketika mereka akhirnya menambahkannya, itu jelas merupakan peningkatan bertahap dengan beberapa kebocoran abstraksi dan daya yang lumpuh. Ternyata, mencampur subtipe dan polimorfisme pada dasarnya sulit; tapi itu bukan alasan mengapa Java Generics bekerja seperti yang mereka lakukan. Mereka bekerja dengan cara yang mereka lakukan karena kendala untuk menjadi peningkatan tambahan untuk versi Java yang lebih lama. Ditto, untuk lebih jauh ke belakang pada hari ketika OOP ditemukan dan orang-orang mulai menulis "obyektif" C (jangan bingung dengan Objective-C), dll. Ingat, C ++ dimulai dengan kedok menjadi superset ketat dari C. Menambahkan paradigma baru selalu membutuhkan pendefinisian bahasa yang baru, atau berakhir dengan kekacauan yang rumit. Maksud saya dalam semua ini adalah bahwa, menambahkan tipe dependen yang sebenarnya ke Haskell akan membutuhkan sejumlah gutting dan restrukturisasi bahasa --- jika kita akan melakukannya dengan benar. Tetapi sangat sulit untuk melakukan perbaikan seperti itu, sedangkan kemajuan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. C ++ dimulai dengan kedok sebagai superset ketat dari C. Menambahkan paradigma baru selalu membutuhkan pendefinisian bahasa yang baru, atau berakhir dengan kekacauan yang rumit. Maksud saya dalam semua ini adalah bahwa, menambahkan tipe dependen yang sebenarnya ke Haskell akan membutuhkan sejumlah gutting dan restrukturisasi bahasa --- jika kita akan melakukannya dengan benar. Tetapi sangat sulit untuk melakukan perbaikan seperti itu, sedangkan kemajuan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. C ++ dimulai dengan kedok sebagai superset ketat dari C. Menambahkan paradigma baru selalu membutuhkan pendefinisian bahasa yang baru, atau berakhir dengan kekacauan yang rumit. Maksud saya dalam semua ini adalah bahwa, menambahkan tipe dependen yang sebenarnya ke Haskell akan membutuhkan sejumlah gutting dan restrukturisasi bahasa --- jika kita akan melakukannya dengan benar. Tetapi sangat sulit untuk melakukan perbaikan seperti itu, sedangkan kemajuan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. atau berakhir dengan kekacauan yang rumit. Maksud saya dalam semua ini adalah bahwa, menambahkan tipe dependen yang sebenarnya ke Haskell akan membutuhkan sejumlah gutting dan restrukturisasi bahasa --- jika kita akan melakukannya dengan benar. Tetapi sangat sulit untuk melakukan perbaikan seperti itu, sedangkan kemajuan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. atau berakhir dengan kekacauan yang rumit. Maksud saya dalam semua ini adalah bahwa, menambahkan tipe dependen yang sebenarnya ke Haskell akan membutuhkan sejumlah gutting dan restrukturisasi bahasa --- jika kita akan melakukannya dengan benar. Tetapi sangat sulit untuk melakukan perbaikan seperti itu, sedangkan kemajuan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. Sangat sulit untuk berkomitmen pada perbaikan semacam itu, sedangkan kemajuan tambahan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll. Sangat sulit untuk berkomitmen pada perbaikan semacam itu, sedangkan kemajuan tambahan yang kami buat tampaknya lebih murah dalam jangka pendek. Sungguh, tidak banyak orang yang meretas GHC, tetapi ada sejumlah kode warisan yang cukup untuk tetap hidup. Ini adalah bagian dari alasan mengapa ada begitu banyak bahasa spin-off seperti DDC, Cayenne, Idris, dll.

wren romano
sumber