Cara terbersih untuk melaporkan kesalahan di Haskell

22

Saya sedang belajar Haskell, dan saya telah menemukan tiga cara berbeda dalam berurusan dengan kesalahan dalam fungsi yang saya tulis:

  1. Saya hanya bisa menulis error "Some error message.", yang melempar pengecualian.
  2. Saya dapat mengembalikan fungsi saya Maybe SomeType, di mana saya mungkin atau tidak dapat mengembalikan apa yang ingin saya kembalikan.
  3. Saya dapat memiliki fungsi saya kembali Either String SomeType, di mana saya dapat mengembalikan pesan kesalahan atau apa yang saya diminta untuk kembali di tempat pertama.

Pertanyaan saya adalah: Metode penanganan kesalahan apa yang harus saya gunakan, dan mengapa? Mungkin saya harus menggunakan metode yang berbeda, tergantung pada konteksnya?

Pemahaman saya saat ini adalah:

  • "Sulit" untuk berurusan dengan pengecualian dalam kode murni fungsional, dan di Haskell kita ingin menjaga hal-hal sefungsional mungkin.
  • Kembali Maybe SomeTypeadalah hal yang tepat untuk dilakukan jika fungsi tersebut akan gagal atau berhasil (yaitu, tidak ada cara yang berbeda dapat gagal ).
  • Kembali Either String SomeTypeadalah hal yang tepat untuk dilakukan jika suatu fungsi dapat gagal dalam salah satu dari berbagai cara.
CmdrMoozy
sumber

Jawaban:

32

Baiklah, aturan pertama penanganan kesalahan di Haskell: Jangan pernah gunakanerror .

Hanya saja mengerikan dalam segala hal. Itu ada murni sebagai tindakan sejarah dan fakta bahwa Prelude menggunakannya mengerikan. Jangan gunakan itu.

Satu-satunya waktu yang bisa Anda gunakan adalah ketika ada sesuatu yang begitu mengerikan secara internal sehingga ada sesuatu yang salah dengan tatanan realitas, sehingga menghasilkan hasil dari program Anda.

Sekarang pertanyaannya menjadi Maybevs Either. Maybecocok untuk sesuatu seperti head, yang mungkin atau mungkin tidak mengembalikan nilai tetapi hanya ada satu alasan yang mungkin untuk gagal. Nothingmengatakan sesuatu seperti "itu rusak, dan Anda sudah tahu mengapa". Beberapa akan mengatakan itu menunjukkan fungsi parsial.

Bentuk penanganan kesalahan yang paling kuat adalah Either+ sebuah kesalahan ADT.

Sebagai contoh di salah satu penyusun hobi saya, saya punya sesuatu seperti

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Sekarang saya mendefinisikan banyak jenis kesalahan, menaruhnya sehingga saya memiliki satu kesalahan tingkat atas yang mulia. Ini bisa menjadi kesalahan dari setiap tahap kompilasi atau ImpossibleError, yang menandakan bug kompilator.

Masing-masing jenis kesalahan ini mencoba untuk menyimpan informasi sebanyak mungkin selama mungkin untuk pencetakan cantik atau analisis lainnya. Lebih penting lagi, dengan tidak memiliki string saya bisa menguji bahwa menjalankan program yang diketik melalui pemeriksa tipe benar-benar menghasilkan kesalahan unifikasi! Setelah sesuatu adalah String, itu hilang selamanya dan setiap informasi yang dikandungnya tidak jelas untuk kompiler / tes, jadi Either Stringtidak bagus juga.

Akhirnya saya kemas tipe ini ExceptT, trafo monad baru dari MTL. Ini pada dasarnya EitherTdan dilengkapi dengan sejumlah fungsi yang bagus untuk melempar dan menangkap kesalahan dengan cara yang murni dan menyenangkan.

Akhirnya, perlu disebutkan bahwa Haskell memiliki mekanisme untuk mendukung penanganan pengecualian seperti bahasa lain, kecuali bahwa menangkap pengecualian tinggal IO. Saya tahu beberapa orang suka menggunakan ini untuk IOaplikasi berat di mana semuanya berpotensi gagal, tetapi sangat jarang sehingga mereka tidak suka memikirkannya. Apakah Anda menggunakan pengecualian tidak murni ini atau hanya ExceptT Error IObenar-benar masalah selera. Secara pribadi saya memilih ExceptTkarena saya suka diingatkan akan kemungkinan gagal.


Sebagai ringkasan,

  • Maybe - Saya bisa gagal dalam satu cara yang jelas
  • Either CustomType - Saya bisa gagal, dan saya akan memberi tahu Anda apa yang terjadi
  • IO+ pengecualian - Terkadang saya gagal. Periksa dokumen saya untuk melihat apa yang saya lemparkan ketika saya melakukannya
  • error - Aku benci kamu juga pengguna
Daniel Gratzer
sumber
Jawaban ini sangat jelas bagi saya. Saya menebak-nebak diri saya berdasarkan fakta bahwa fungsi bawaan suka headatau lastsepertinya digunakan error, jadi saya bertanya-tanya apakah itu sebenarnya cara yang baik untuk melakukan sesuatu dan saya hanya melewatkan sesuatu. Ini menjawab pertanyaan itu. :)
CmdrMoozy
5
@CmdrMoozy Senang membantu :) Sangat disayangkan bahwa pembuka memiliki praktik buruk. Begitulah cara warisan: /
Daniel Gratzer
3
+1 Ringkasan Anda luar biasa
recursion.ninja
2

Saya tidak akan memisahkan 2 dan 3 berdasarkan berapa banyak cara sesuatu dapat gagal. Segera setelah Anda berpikir hanya ada satu cara yang mungkin, yang lain akan menunjukkan wajahnya. Metriknya seharusnya "apakah penelepon saya peduli mengapa sesuatu gagal? Bisakah mereka benar-benar melakukan sesuatu tentang hal itu?".

Di luar itu, tidak jelas bagi saya yang Either String SomeTypedapat menghasilkan kondisi kesalahan. Saya akan membuat tipe data aljabar sederhana dengan kondisi dengan nama yang lebih deskriptif.

Yang Anda gunakan tergantung pada sifat masalah yang Anda hadapi, dan idiom dari paket perangkat lunak yang Anda gunakan. Padahal saya cenderung menghindari # 1.

Telastyn
sumber
Sebenarnya, menggunakan Eitheruntuk kesalahan adalah pola yang sangat terkenal di Haskell.
Rufflewind
1
@rufflewind - Eitherbukan bagian yang tidak jelas, penggunaan Stringsebagai kesalahan daripada string yang sebenarnya.
Telastyn
Ah, saya salah membaca maksud Anda.
Rufflewind