Tampaknya Template Haskell sering dipandang oleh komunitas Haskell sebagai kenyamanan yang tidak menguntungkan. Sulit untuk mengatakan dengan tepat apa yang telah saya amati dalam hal ini, tetapi pertimbangkan beberapa contoh ini
- Template Haskell terdaftar di bawah "The Ugly (but needed)" sebagai jawaban atas pertanyaan. Ekstensi Haskell (GHC) mana yang harus digunakan / dihindari pengguna?
- Template Haskell dianggap sebagai solusi sementara / inferior di Vektor Tanpa Kotak dari utas nilai newtype'd (milis perpustakaan)
- Yesod sering dikritik karena terlalu mengandalkan Template Haskell (lihat posting blog dalam menanggapi sentimen ini)
Saya telah melihat berbagai posting blog di mana orang melakukan hal-hal yang cukup rapi dengan Template Haskell, memungkinkan sintaksis yang lebih cantik yang tidak mungkin dilakukan dalam Haskell biasa, serta pengurangan boilerplate yang luar biasa. Jadi mengapa Template Haskell dipandang rendah dengan cara ini? Apa yang membuatnya tidak diinginkan? Dalam keadaan apa Templat Haskell harus dihindari, dan mengapa?
haskell
template-haskell
Dan Burton
sumber
sumber
Jawaban:
Salah satu alasan untuk menghindari Template Haskell adalah karena ia secara keseluruhan tidak aman untuk semua, sehingga bertentangan dengan "semangat Haskell." Berikut ini beberapa contohnya:
Exp
, tetapi Anda tidak tahu apakah itu ekspresi yang mewakili[Char]
atau(a -> (forall b . b -> c))
atau apa pun. TH akan lebih dapat diandalkan jika seseorang dapat menyatakan bahwa suatu fungsi hanya dapat menghasilkan ekspresi dari jenis tertentu, atau hanya deklarasi fungsi, atau hanya pola pencocokan konstruktor data, dll.foo
yang tidak ada? Beruntung, Anda hanya akan melihat bahwa ketika benar-benar menggunakan generator kode Anda, dan hanya dalam keadaan yang memicu pembuatan kode tertentu. Sangat sulit untuk menguji unit juga.TH juga berbahaya:
IO
, termasuk meluncurkan rudal atau mencuri kartu kredit Anda. Anda tidak ingin harus melihat melalui setiap paket komplotan rahasia yang pernah Anda unduh untuk mencari eksploitasi TH.Lalu ada beberapa masalah yang membuat fungsi TH kurang menyenangkan untuk digunakan sebagai pengembang perpustakaan:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
hanya sebuah monad, sehingga Anda dapat menggunakan semua fungsi yang biasa di atasnya. Beberapa orang tidak mengetahui hal ini, dan karena itu, mereka membuat beberapa versi kelebihan beban dari fungsi yang sama dengan fungsi yang sama, dan fungsi-fungsi ini menyebabkan efek mengasapi tertentu. Juga, kebanyakan orang menulis generator mereka diQ
monad bahkan ketika mereka tidak perlu, yang seperti menulisbla :: IO Int; bla = return 3
; Anda memberi fungsi lebih "lingkungan" daripada yang dibutuhkan, dan klien fungsi tersebut diminta untuk menyediakan lingkungan itu sebagai akibatnya.Akhirnya, ada beberapa hal yang membuat fungsi TH kurang menyenangkan untuk digunakan sebagai pengguna akhir:
Q Dec
, ia dapat menghasilkan apa pun di tingkat atas modul, dan Anda sama sekali tidak memiliki kendali atas apa yang akan dihasilkan.sumber
Ini semata-mata pendapat saya sendiri.
Ini jelek untuk digunakan.
$(fooBar ''Asdf)
tidak terlihat bagus. Dangkal, tentu, tetapi berkontribusi.Bahkan lebih buruk untuk menulis. Mengutip kadang-kadang berhasil, tetapi sering kali Anda harus melakukan pencangkokan dan pemipaan AST manual. The API besar dan berat, selalu ada banyak kasus Anda tidak peduli tentang tapi masih perlu pengiriman, dan kasus Anda peduli tentang cenderung untuk hadir dalam berbagai bentuk serupa tapi tak sama (data vs newtype, merekam -style vs konstruktor normal, dan sebagainya). Membosankan dan berulang-ulang untuk menulis dan cukup rumit untuk tidak mekanis. The reformasi usulan alamat beberapa ini (membuat tanda kutip lebih luas berlaku).
Pembatasan panggung adalah neraka. Tidak dapat memisahkan fungsi-fungsi yang didefinisikan dalam modul yang sama adalah bagian yang lebih kecil darinya: konsekuensi lainnya adalah jika Anda memiliki sambungan tingkat atas, semua yang ada di dalam modul akan berada di luar ruang lingkup apa pun sebelumnya. Bahasa lain dengan properti ini (C, C ++) membuatnya bisa diterapkan dengan memungkinkan Anda untuk meneruskan mendeklarasikan sesuatu, tetapi Haskell tidak. Jika Anda memerlukan referensi siklik antara deklarasi yang disambungkan atau dependensi dan tanggungannya, Anda biasanya hanya kacau.
Itu tidak disiplin. Yang saya maksud dengan ini adalah bahwa sebagian besar waktu ketika Anda mengekspresikan abstraksi, ada semacam prinsip atau konsep di balik abstraksi itu. Untuk banyak abstraksi, prinsip di belakangnya dapat dinyatakan dalam tipenya. Untuk kelas tipe, Anda sering dapat merumuskan undang-undang yang harus dipatuhi oleh mesin virtual dan klien dapat memikul Jika Anda menggunakan fitur generik baru GHC untuk mengabstraksi bentuk deklarasi instance atas tipe data apa pun (dalam batas), Anda bisa mengatakan "untuk tipe penjumlahan, kerjanya seperti ini, untuk tipe produk, berfungsi seperti itu". Templat Haskell, di sisi lain, hanyalah makro. Ini bukan abstraksi pada level ide, tetapi abstraksi pada level AST, yang lebih baik, tetapi hanya sedikit, daripada abstraksi pada level teks biasa. *
Itu mengikat Anda ke GHC. Secara teori kompiler lain dapat mengimplementasikannya, tetapi dalam praktiknya saya ragu ini akan pernah terjadi. (Ini berbeda dengan berbagai jenis ekstensi sistem yang, meskipun mungkin hanya diimplementasikan oleh GHC saat ini, saya dapat dengan mudah membayangkan diadopsi oleh kompiler lain di ujung jalan dan akhirnya distandarisasi.)
API tidak stabil. Ketika fitur bahasa baru ditambahkan ke GHC dan paket template-haskell diperbarui untuk mendukungnya, ini sering melibatkan perubahan yang tidak kompatibel ke belakang untuk tipe data TH. Jika Anda ingin kode TH Anda kompatibel dengan lebih dari satu versi GHC, Anda harus sangat berhati-hati dan mungkin menggunakannya
CPP
.Ada prinsip umum bahwa Anda harus menggunakan alat yang tepat untuk pekerjaan dan yang terkecil yang akan mencukupi, dan dalam analogi itu Template Haskell adalah sesuatu seperti ini . Jika ada cara untuk melakukannya bukan Template Haskell, itu umumnya lebih disukai.
Keuntungan dari Template Haskell adalah Anda dapat melakukan hal-hal yang tidak dapat Anda lakukan dengan cara lain, dan ini adalah hal yang besar. Sebagian besar hal-hal yang digunakan untuk TH hanya dapat dilakukan jika mereka diterapkan secara langsung sebagai fitur kompiler. TH sangat bermanfaat untuk memiliki keduanya karena memungkinkan Anda melakukan hal-hal ini, dan karena memungkinkan Anda membuat prototipe ekstensi kompiler potensial dengan cara yang jauh lebih ringan dan dapat digunakan kembali (lihat berbagai paket lensa, misalnya).
Untuk meringkas mengapa saya pikir ada perasaan negatif terhadap Template Haskell: Ini memecahkan banyak masalah, tetapi untuk masalah apa pun yang diselesaikan, rasanya harus ada solusi yang lebih baik, lebih elegan, dan berdisiplin yang lebih cocok untuk menyelesaikan masalah itu, salah satu yang tidak menyelesaikan masalah dengan secara otomatis menghasilkan boilerplate, tetapi dengan menghilangkan kebutuhan untuk memiliki boilerplate.
* Meskipun saya sering merasa
CPP
memiliki rasio power-to-weight yang lebih baik untuk masalah-masalah yang dapat diselesaikan.EDIT 23-04-14: Apa yang sering saya coba sampaikan di atas, dan baru-baru ini, adalah bahwa ada perbedaan penting antara abstraksi dan deduplikasi. Abstraksi yang tepat sering menghasilkan deduplikasi sebagai efek samping, dan duplikasi sering kali merupakan pertanda abstraksi yang tidak memadai, tetapi bukan karena itu bernilai. Abstraksi yang tepat adalah apa yang membuat kode menjadi benar, dapat dipahami, dan dapat dipelihara. Deduplikasi hanya membuatnya lebih pendek. Template Haskell, seperti makro pada umumnya, adalah alat untuk deduplikasi.
sumber
Saya ingin membahas beberapa poin yang diangkat dflemstr.
Saya tidak menemukan fakta bahwa Anda tidak dapat mengetik TH untuk mengkhawatirkan itu. Mengapa? Karena walaupun ada kesalahan, itu masih akan mengkompilasi waktu. Saya tidak yakin apakah ini memperkuat argumen saya, tetapi semangat ini serupa dengan kesalahan yang Anda terima saat menggunakan templat di C ++. Saya pikir kesalahan ini lebih dimengerti daripada kesalahan C ++, karena Anda akan mendapatkan versi cetak yang cukup dari kode yang dihasilkan.
Jika ungkapan TH / quasi-quoter melakukan sesuatu yang sangat canggih sehingga sudut-sudut rumit bisa bersembunyi, maka mungkin itu keliru?
Saya sedikit melanggar aturan ini dengan quasi-quoters yang saya kerjakan belakangan ini (menggunakan haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Saya tahu ini memperkenalkan beberapa bug seperti tidak dapat menyatukan dalam pemahaman daftar umum. Namun, saya berpikir bahwa ada peluang bagus bahwa beberapa ide di http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal akan berakhir di kompiler. Sampai saat itu, perpustakaan untuk mengurai Haskell ke pohon TH adalah pendekatan yang hampir sempurna.
Mengenai kecepatan kompilasi / dependensi, kita dapat menggunakan paket "nol" untuk memasukkan kode yang dihasilkan. Ini setidaknya bagus untuk pengguna perpustakaan yang diberikan, tetapi kami tidak dapat melakukan jauh lebih baik untuk kasus mengedit perpustakaan. Bisakah dependensi TH mengasup binari yang dihasilkan? Saya pikir itu meninggalkan semua yang tidak direferensikan oleh kode yang dikompilasi.
Pembatasan pementasan / pemisahan langkah-langkah kompilasi dari modul Haskell tidak menyedot.
RE Opacity: Ini sama untuk setiap fungsi perpustakaan yang Anda panggil. Anda tidak memiliki kendali atas apa yang akan dilakukan Data.List.groupBy. Anda hanya memiliki "jaminan" / konvensi yang masuk akal bahwa nomor versi memberi tahu Anda sesuatu tentang kompatibilitas. Ini agak berbeda dari masalah kapan.
Di sinilah penggunaan zeroth terbayar - Anda sudah membuat versi file yang dihasilkan - sehingga Anda akan selalu tahu kapan bentuk kode yang dihasilkan telah berubah. Melihat diffs mungkin sedikit sulit, untuk sejumlah besar kode yang dihasilkan, jadi itu satu tempat di mana antarmuka pengembang yang lebih baik akan berguna.
RE Monolithism: Anda tentu saja dapat memposting-proses hasil dari ekspresi TH, menggunakan kode waktu kompilasi Anda sendiri. Tidak akan terlalu banyak kode untuk difilter pada tipe / nama deklarasi tingkat atas. Heck, Anda bisa membayangkan menulis fungsi yang melakukan ini secara umum. Untuk memodifikasi / menghapus kuasiquoter kuasi, Anda dapat mencocokkan pola pada "QuasiQuoter" dan mengekstrak transformasi yang digunakan, atau membuat yang baru dalam hal yang lama.
sumber
[Dec]
dan menghapus hal-hal yang tidak Anda inginkan, tetapi katakanlah fungsi membaca file definisi eksternal ketika membuat antarmuka JSON. Hanya karena Anda tidak menggunakan ituDec
tidak membuat generator berhenti mencari file definisi, membuat kompilasi gagal. Karena alasan itu, alangkah baiknya memiliki versiQ
monad yang lebih ketat yang memungkinkan Anda menghasilkan nama baru (dan hal-hal semacam itu) tetapi tidak mengizinkanIO
, sehingga, seperti yang Anda katakan, orang dapat memfilter hasilnya / melakukan komposisi lain .Jawaban ini adalah jawaban atas masalah yang dikemukakan oleh illissius, poin demi poin:
Saya setuju. Saya merasa $ () dipilih agar terlihat seperti bagian dari bahasa - menggunakan palet simbol Haskell yang sudah dikenal. Namun, itulah yang Anda / tidak / inginkan dalam simbol yang digunakan untuk penyambungan makro Anda. Mereka pasti berbaur terlalu banyak, dan aspek kosmetik ini sangat penting. Saya suka tampilan {{}} untuk splices, karena keduanya sangat berbeda secara visual.
Saya juga setuju dengan ini, namun, seperti yang diperhatikan oleh beberapa komentar di "Petunjuk Arah Baru untuk TH", kurangnya kutipan AST yang bagus di luar kotak bukanlah cacat kritis. Dalam paket WIP ini, saya berupaya mengatasi masalah ini dalam bentuk pustaka: https://github.com/mgsloan/quasi-extras . Sejauh ini saya mengizinkan splicing di beberapa tempat lebih banyak dari biasanya dan dapat mencocokkan pola pada AST.
Saya pernah mengalami masalah definisi TH siklik menjadi tidak mungkin sebelumnya ... Ini sangat mengganggu. Ada solusi, tapi jelek - bungkus hal-hal yang terlibat dalam ketergantungan siklik dalam ekspresi TH yang menggabungkan semua deklarasi yang dihasilkan. Salah satu dari deklarasi generator ini bisa saja berupa quasi-quoter yang menerima kode Haskell.
Itu hanya tidak berprinsip jika Anda melakukan hal-hal yang tidak berprinsip dengannya. Satu-satunya perbedaan adalah bahwa dengan compiler menerapkan mekanisme untuk abstraksi, Anda lebih percaya bahwa abstraksi tidak bocor. Mungkin demokratisasi desain bahasa memang terdengar agak menakutkan! Pencipta perpustakaan TH perlu mendokumentasikan dengan baik dan jelas mendefinisikan arti dan hasil dari alat yang mereka berikan. Sebuah contoh yang baik dari berprinsip TH adalah paket turunan : http://hackage.haskell.org/package/derive - ia menggunakan DSL sehingga contoh dari banyak derivasi / menentukan / derivasi yang sebenarnya.
Itu poin yang cukup bagus - API TH cukup besar dan kikuk. Mengimplementasikan ulang sepertinya bisa jadi sulit. Namun, hanya ada beberapa cara untuk mengiris masalah mewakili ASK Haskell. Saya membayangkan bahwa menyalin ADT TH, dan menulis konverter ke representasi AST internal akan membuat Anda mendapatkan banyak jalan di sana. Ini akan setara dengan upaya (tidak signifikan) untuk menciptakan haskell-src-meta. Ini juga bisa diimplementasikan kembali dengan mencetak THT AST dan menggunakan parser internal kompiler.
Walaupun saya bisa saja salah, saya tidak melihat TH sebagai ekstensi kompiler yang rumit, dari perspektif implementasi. Ini sebenarnya salah satu manfaat dari "menjaganya tetap sederhana" dan tidak memiliki lapisan dasar menjadi beberapa sistem templating yang secara teoritis menarik dan dapat diverifikasi secara statis.
Ini juga poin yang bagus, tetapi agak dramatis. Meskipun ada penambahan API akhir-akhir ini, mereka belum menginduksi kerusakan secara luas. Juga, saya berpikir bahwa dengan kutipan AST superior yang saya sebutkan sebelumnya, API yang benar-benar perlu digunakan dapat dikurangi secara substansial. Jika tidak ada konstruksi / pencocokan membutuhkan fungsi yang berbeda, dan sebaliknya dinyatakan sebagai literal, maka sebagian besar API menghilang. Selain itu, kode yang Anda tulis akan port lebih mudah ke representasi AST untuk bahasa yang mirip dengan Haskell.
Singkatnya, saya berpikir bahwa TH adalah alat yang kuat, semi-diabaikan. Lebih sedikit kebencian dapat mengarah pada eko-sistem perpustakaan yang lebih hidup, mendorong penerapan prototipe fitur bahasa yang lebih banyak. Telah diamati bahwa TH adalah alat yang dikuasai, yang dapat membuat Anda / melakukan / hampir apa saja. Anarki! Ya, menurut saya kekuatan ini memungkinkan Anda untuk mengatasi sebagian besar keterbatasannya, dan membangun sistem yang mampu melakukan pendekatan meta-pemrograman yang cukup berprinsip. Sebaiknya gunakan hacks jelek untuk mensimulasikan implementasi yang "tepat", karena dengan cara ini desain implementasi yang "tepat" akan secara bertahap menjadi jelas.
Dalam versi nirwana ideal pribadi saya, banyak bahasa yang benar-benar akan keluar dari kompiler, ke perpustakaan-perpustakaan dari varietas ini. Fakta bahwa fitur diimplementasikan sebagai perpustakaan tidak banyak mempengaruhi kemampuan mereka untuk setia abstrak.
Apa jawaban khas Haskell untuk kode boilerplate? Abstraksi. Apa abstraksi favorit kita? Fungsi dan typeclasses!
Typeclasses mari kita mendefinisikan satu set metode, yang kemudian dapat digunakan dalam segala macam fungsi generik pada kelas itu. Namun, selain ini, satu-satunya cara kelas membantu menghindari boilerplate adalah dengan menawarkan "definisi default". Sekarang ini adalah contoh fitur yang tidak berprinsip!
Set pengikat minimal tidak dapat dideklarasikan / dikompilasi. Ini bisa mengarah pada definisi yang tidak sengaja yang menghasilkan bottom karena rekursi timbal balik.
Terlepas dari kemudahan dan kekuatan yang akan dihasilkan, Anda tidak dapat menentukan standar superclass, karena instance yatim http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Ini akan memungkinkan kami memperbaiki hierarki numerik dengan anggun!
Mengejar kemampuan seperti-TH untuk standar metode mengarah ke http://www.haskell.org/haskellwiki/GHC.Generics . Meskipun ini adalah hal yang keren, kode debugging satu-satunya pengalaman saya yang menggunakan generik ini hampir tidak mungkin, karena ukuran tipe yang diinduksi untuk dan ADT serumit AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
Dengan kata lain, ini mengikuti fitur yang disediakan oleh TH, tetapi harus mengangkat seluruh domain bahasa, bahasa konstruksi, ke dalam representasi sistem tipe. Walaupun saya dapat melihatnya bekerja dengan baik untuk masalah umum Anda, untuk masalah yang kompleks, tampaknya cenderung menghasilkan tumpukan simbol yang jauh lebih mengerikan daripada peretasan TH.
TH memberi Anda perhitungan waktu-kompilasi tingkat-nilai dari kode output, sedangkan generik memaksa Anda untuk mengangkat bagian pencocokan pola / rekursi dari kode ke dalam sistem tipe. Meskipun ini membatasi pengguna dalam beberapa cara yang cukup berguna, saya tidak berpikir kompleksitasnya sepadan.
Saya berpikir bahwa penolakan TH dan metaprogramming seperti lisp mengarah ke preferensi terhadap hal-hal seperti metode-default daripada lebih fleksibel, ekspansi makro seperti deklarasi instance. Disiplin menghindari hal-hal yang dapat menyebabkan hasil yang tidak terduga adalah bijaksana, namun, kita tidak boleh mengabaikan bahwa sistem tipe Haskell yang memungkinkan memungkinkan metaprogramming yang lebih andal daripada di banyak lingkungan lain (dengan memeriksa kode yang dihasilkan).
sumber
Satu masalah yang agak pragmatis dengan Template Haskell adalah bahwa ia hanya berfungsi ketika interteter bytecode GHC tersedia, yang tidak berlaku pada semua arsitektur. Jadi jika program Anda menggunakan Haskell Templat atau bergantung pada perpustakaan yang menggunakannya, itu tidak akan berjalan pada mesin dengan ARM, MIPS, S390 atau PowerPC CPU.
Ini relevan dalam praktiknya: git-annex adalah alat yang ditulis dalam Haskell yang masuk akal untuk dijalankan pada mesin yang mengkhawatirkan penyimpanan, mesin tersebut sering memiliki non-i386-CPU. Secara pribadi, saya menjalankan git-annex pada NSLU 2 (32 MB RAM, 266MHz CPU; apakah Anda tahu Haskell berfungsi dengan baik pada perangkat keras seperti itu?) Jika itu akan menggunakan Template Haskell, ini tidak mungkin.
(Situasi tentang GHC pada ARM meningkat banyak hari ini dan saya pikir 7.4.2 bahkan bekerja, tetapi intinya masih berlaku).
sumber
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)Mengapa TH buruk? Bagi saya, begini:
Pikirkan tentang itu. Setengah daya tarik dari Haskell adalah bahwa desain tingkat tinggi memungkinkan Anda untuk menghindari sejumlah besar kode boilerplate yang tidak berguna yang harus Anda tulis dalam bahasa lain. Jika Anda memerlukan pembuatan kode waktu kompilasi, pada dasarnya Anda mengatakan bahwa bahasa atau desain aplikasi Anda gagal. Dan kami programmer tidak suka gagal.
Terkadang, tentu saja, itu perlu. Tetapi kadang-kadang Anda dapat menghindari kebutuhan TH dengan menjadi sedikit lebih pintar dengan desain Anda.
(Yang lain adalah bahwa TH adalah level yang cukup rendah. Tidak ada desain tingkat tinggi yang agung; banyak detail implementasi internal GHC terekspos. Dan itu membuat API mudah berubah ...)
sumber