Apa manfaat memiliki "tidak ada pengecualian runtime", seperti klaim Elm?

16

Beberapa bahasa mengklaim tidak memiliki "pengecualian runtime" sebagai keunggulan yang jelas dari bahasa lain yang memilikinya.

Saya bingung tentang masalah ini.

Pengecualian runtime hanyalah alat, sejauh yang saya tahu, dan bila digunakan dengan baik:

  • Anda dapat mengomunikasikan status "kotor" (melempar data yang tidak terduga)
  • menambahkan tumpukan Anda dapat menunjuk ke rantai kesalahan
  • Anda dapat membedakan antara kekacauan (mis. mengembalikan nilai kosong pada input yang tidak valid) dan penggunaan yang tidak aman yang membutuhkan perhatian pengembang (mis. melempar pengecualian pada input yang tidak valid)
  • Anda dapat menambahkan detail ke kesalahan Anda dengan pesan pengecualian yang memberikan rincian bermanfaat lebih lanjut membantu upaya debugging (secara teoritis)

Di sisi lain saya merasa sangat sulit untuk men-debug perangkat lunak yang "menelan" pengecualian. Misalnya

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

Jadi pertanyaannya adalah: apa keuntungan teoretis yang kuat dari "tidak ada pengecualian runtime"?


Contoh

https://guide.elm-lang.org/

Dalam praktiknya tidak ada kesalahan runtime. Tidak nol. Tidak terdefinisi bukan fungsi.

Atoth
sumber
Saya pikir ini akan membantu untuk memberikan contoh atau dua bahasa yang Anda maksud dan / atau tautan ke klaim tersebut. Ini dapat ditafsirkan dalam beberapa cara.
JimmyJames
Contoh terakhir yang saya temukan adalah elm dibandingkan dengan C, C ++, C #, Java, ECMAScript, dll. Saya telah memperbarui pertanyaan saya @JimmyJames
atoth
2
Ini adalah yang pertama saya dengar. Reaksi pertama saya adalah menelepon BS. Perhatikan kata-kata musang: dalam praktiknya
JimmyJames
@atoth Saya akan mengedit judul pertanyaan Anda untuk membuatnya lebih jelas, karena ada beberapa pertanyaan yang tidak terkait yang mirip dengan itu (seperti "RuntimeException" vs "Pengecualian" di Jawa). Jika Anda tidak menyukai judul yang baru, silakan mengeditnya lagi.
Andres F.
OK, saya ingin itu cukup umum, tapi saya bisa setuju kalau itu membantu, terima kasih atas kontribusi Anda @AndresF.!
atoth

Jawaban:

28

Pengecualian memiliki semantik yang sangat terbatas. Mereka harus ditangani tepat di mana mereka dilemparkan, atau di tumpukan panggilan langsung ke atas, dan tidak ada indikasi kepada programmer pada waktu kompilasi jika Anda lupa melakukannya.

Bandingkan ini dengan Elm di mana kesalahan dikodekan sebagai Hasil atau Maybes , yang keduanya nilai . Itu berarti Anda mendapatkan kesalahan kompiler jika Anda tidak menangani kesalahan. Anda dapat menyimpannya dalam variabel atau bahkan koleksi untuk menunda penanganannya ke waktu yang nyaman. Anda dapat membuat fungsi untuk menangani kesalahan dengan cara khusus aplikasi alih-alih mengulangi blok try-catch yang sangat mirip di semua tempat. Anda dapat mengaitkannya ke dalam perhitungan yang hanya berhasil jika semua bagiannya berhasil, dan mereka tidak harus dijejalkan ke dalam satu blok uji coba. Anda tidak dibatasi oleh sintaks bawaan.

Ini tidak seperti "menelan pengecualian." Itu membuat kondisi kesalahan eksplisit dalam sistem tipe dan menyediakan semantik alternatif yang jauh lebih fleksibel untuk menanganinya.

Perhatikan contoh berikut. Anda dapat menempelkan ini ke http://elm-lang.org/try jika Anda ingin melihatnya beraksi.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Perhatikan String.toIntdi calculatefungsi memiliki kemungkinan gagal. Di Jawa, ini berpotensi melempar pengecualian runtime. Saat membaca input pengguna, ia memiliki peluang yang cukup bagus. Elm malah memaksa saya untuk menghadapinya dengan mengembalikan Result, tapi perhatikan saya tidak harus menghadapinya segera. Saya dapat menggandakan input dan mengonversinya menjadi string, kemudian memeriksa input yang buruk dalam getDefaultfungsi. Tempat ini jauh lebih cocok untuk pemeriksaan daripada titik di mana kesalahan terjadi atau naik di tumpukan panggilan.

Cara kompiler memaksa tangan kita juga jauh lebih bagus daripada pengecualian Java yang diperiksa. Anda perlu menggunakan fungsi yang sangat spesifik Result.withDefaultuntuk mengekstraksi nilai yang Anda inginkan. Meskipun secara teknis Anda bisa menyalahgunakan mekanisme semacam itu, tidak ada gunanya. Karena Anda dapat menunda keputusan sampai Anda tahu pesan default / error yang baik untuk dimasukkan, tidak ada alasan untuk tidak menggunakannya.

Karl Bielefeldt
sumber
8
That means you get a compiler error if you don't handle the error.- Nah, itulah alasan di balik Pengecualian Diperiksa di Jawa, tapi kita semua tahu seberapa baik itu berhasil.
Robert Harvey
4
@RobertHarvey Di satu sisi, pengecualian diperiksa Java adalah versi orang miskin ini. Sayangnya mereka bisa "ditelan" (seperti pada contoh OP). Mereka juga bukan tipe nyata, membuat mereka jalur tambahan yang asing untuk aliran kode. Bahasa dengan jenis sistem yang lebih baik memungkinkan Anda untuk kesalahan encode sebagai nilai-nilai kelas pertama, yang adalah apa yang Anda lakukan di (katakanlah) Haskell dengan Maybe, Either, dll Elm terlihat seperti itu mengambil halaman dari bahasa seperti ML, OCaml atau Haskell.
Andres F.
3
@ Jimim James Tidak, mereka tidak memaksamu. Anda hanya perlu "menangani" kesalahan saat Anda ingin menggunakan nilainya. Jika saya melakukannya x = some_func(), saya tidak perlu melakukan apa pun kecuali saya ingin memeriksa nilainya x, dalam hal ini saya dapat memeriksa apakah saya memiliki kesalahan atau nilai "valid"; Selain itu, ini adalah kesalahan jenis statis untuk mencoba menggunakan satu di tempat yang lain, jadi saya tidak bisa melakukannya. Jika tipe Elm berfungsi seperti bahasa fungsional lainnya, saya sebenarnya dapat melakukan hal-hal seperti menulis nilai dari fungsi yang berbeda bahkan sebelum saya tahu apakah itu kesalahan atau tidak! Ini adalah tipikal dari bahasa FP.
Andres F.
6
@atoth Tapi Anda jangan memiliki keuntungan yang signifikan dan ada adalah alasan yang sangat bagus (seperti yang telah dijelaskan dalam beberapa jawaban untuk pertanyaan Anda). Saya benar-benar mendorong Anda untuk belajar bahasa dengan sintaks seperti ML dan Anda akan melihat betapa membebaskannya untuk menyingkirkan sintaks mirip C (ML, omong-omong, dikembangkan pada awal 70-an, yang membuatnya kira-kira sezaman dengan C). Orang-orang yang mendesain sistem jenis ini menganggap sintaksis seperti ini normal, tidak seperti C :) Saat Anda melakukannya, tidak ada salahnya untuk mempelajari Lisp juga :)
Andres F.
6
@atoth Jika Anda ingin mengambil satu hal dari semua ini, ambil yang ini: selalu pastikan Anda tidak menjadi mangsa Blub Paradox . Jangan jengkel pada sintaks baru. Mungkin ada di sana karena fitur-fitur canggih yang tidak Anda kenal :)
Andres F.
10

Untuk memahami pernyataan ini, pertama-tama kita harus memahami apa yang dibeli oleh sistem tipe statis. Pada dasarnya, apa yang diberikan oleh sistem tipe statis adalah jaminan: jika tipe program memeriksa, kelas perilaku runtime tertentu tidak dapat terjadi.

Kedengarannya menyenangkan. Ya, pemeriksa tipe mirip dengan pemeriksa teorema. (Sebenarnya, menurut Curry-Howard-Isomorphism, mereka adalah hal yang sama.) Satu hal yang sangat aneh tentang teorema adalah ketika Anda membuktikan teorema, Anda membuktikan dengan tepat apa yang dikatakan teorema, tidak lebih. (Itu misalnya, mengapa, ketika seseorang mengatakan "Saya telah membuktikan program ini benar", Anda harus selalu bertanya "tolong jelaskan 'benar'".) Hal yang sama berlaku untuk sistem ketik. Ketika kita mengatakan "suatu program adalah tipe-aman", yang kami maksud bukanlah bahwa tidak ada kesalahan yang mungkin terjadi. Kita hanya bisa mengatakan bahwa kesalahan yang dijanjikan oleh sistem tipe kita untuk mencegah tidak terjadi.

Jadi, program dapat memiliki banyak perilaku runtime yang tak terhingga. Dari mereka, banyak yang tak terhingga berguna, tetapi juga yang tak terhingga banyak yang "salah" (untuk berbagai definisi "benar"). Suatu sistem tipe statis memungkinkan kita untuk membuktikan bahwa suatu himpunan terbatas yang pasti dan pasti dari banyak perilaku runtime yang tak terhingga tidak dapat terjadi.

Perbedaan antara sistem tipe yang berbeda pada dasarnya adalah di mana, berapa banyak, dan seberapa kompleks perilaku runtime yang mereka buktikan tidak terjadi. Sistem tipe lemah seperti Java hanya dapat membuktikan hal-hal yang sangat mendasar. Sebagai contoh, Java dapat membuktikan bahwa metode yang diketikkan sebagai mengembalikan Stringtidak dapat mengembalikan a List. Tapi, misalnya, dapat tidak membuktikan bahwa metode ini tidak akan tidak kembali. Itu juga tidak dapat membuktikan bahwa metode ini tidak akan memberikan pengecualian. Dan itu tidak dapat membuktikan bahwa itu tidak akan mengembalikan yang salah String  - ada yang Stringakan memenuhi pemeriksa tipe. (Dan, tentu saja, bahkan nullakan memuaskan juga.) Bahkan ada hal-hal yang sangat sederhana bahwa Jawa tidak dapat membuktikan, itulah sebabnya mengapa kita memiliki pengecualian seperti ArrayStoreException, ClassCastException, atau semua orang favorit, yang NullPointerException.

Sistem tipe yang lebih kuat seperti Agda juga dapat membuktikan hal-hal seperti "akan mengembalikan jumlah dari dua argumen" atau "mengembalikan versi daftar yang disortir sebagai argumen".

Sekarang, apa yang dimaksud oleh desainer Elm dengan pernyataan bahwa mereka tidak memiliki pengecualian runtime adalah bahwa sistem tipe Elm dapat membuktikan tidak adanya (sebagian besar dari) perilaku runtime yang dalam bahasa lain tidak dapat dibuktikan tidak terjadi dan dengan demikian dapat menyebabkan untuk perilaku yang salah saat runtime (yang dalam kasus terbaik berarti pengecualian, dalam kasus yang lebih buruk berarti crash, dan dalam kasus terburuk dari semua berarti tidak ada crash, tidak ada pengecualian, dan hanya hasil yang salah secara diam-diam).

Jadi, mereka tidak mengatakan "kami tidak menerapkan pengecualian". Mereka mengatakan "hal-hal yang akan menjadi pengecualian runtime dalam bahasa khas yang dialami oleh programmer khas Elm, ditangkap oleh sistem tipe". Tentu saja, seseorang yang datang dari Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq, atau bahasa serupa akan melihat Elm sebagai sangat lemah dalam perbandingan. Pernyataan ini lebih ditujukan untuk programmer Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl,… programmer khas.

Jörg W Mittag
sumber
5
Catatan untuk editor potensial: Saya sangat menyesal tentang penggunaan negatif ganda dan bahkan tiga kali lipat. Namun, saya sengaja membiarkannya: sistem tipe menjamin tidak adanya jenis perilaku runtime tertentu, yaitu mereka menjamin hal-hal tertentu tidak terjadi. Dan saya ingin menjaga agar formulasi "membuktikan tidak terjadi" utuh, yang sayangnya mengarah pada konstruksi seperti "itu tidak dapat membuktikan bahwa suatu metode tidak akan kembali". Jika Anda dapat menemukan cara untuk meningkatkan ini, silakan, tetapi harap diingat di atas. Terima kasih!
Jörg W Mittag
2
Jawaban yang bagus secara keseluruhan, tetapi satu hal kecil: "type checker mirip dengan teorema pembalik." Sebenarnya, pemeriksa tipe lebih mirip dengan pemeriksa teorema: mereka berdua melakukan verifikasi , bukan deduksi .
gardenhead
4

Elm dapat menjamin tidak ada pengecualian runtime karena alasan yang sama C dapat menjamin tidak ada pengecualian runtime: Bahasa tidak mendukung konsep pengecualian.

Elm memiliki cara memberi sinyal kondisi kesalahan saat runtime, tetapi sistem ini bukan pengecualian, ini adalah "Hasil". Fungsi yang mungkin gagal mengembalikan "Hasil" yang berisi nilai reguler atau kesalahan. Elms sangat diketik, jadi ini eksplisit dalam sistem tipe. Jika suatu fungsi selalu mengembalikan integer, ia memiliki tipe Int. Tetapi jika ia mengembalikan integer atau gagal, tipe pengembaliannya adalah Result Error Int. (String adalah pesan kesalahan.) Ini memaksa Anda untuk secara eksplisit menangani kedua kasus di situs panggilan.

Berikut adalah contoh dari pendahuluan (sedikit disederhanakan):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

Fungsi ini toIntdapat gagal jika input tidak dapat diuraikan, jadi tipe pengembaliannya adalah Result String int. Untuk mendapatkan nilai integer yang sebenarnya, Anda harus "membongkar" melalui pencocokan pola, yang pada gilirannya memaksa Anda untuk menangani kedua kasus.

Hasil dan pengecualian pada dasarnya melakukan hal yang sama, perbedaan penting adalah "default". Pengecualian akan muncul dan menghentikan program secara default, dan Anda harus menangkapnya secara eksplisit jika ingin menanganinya. Hasilnya adalah sebaliknya - Anda dipaksa untuk menanganinya secara default, jadi Anda harus meneruskannya sampai ke puncak jika Anda ingin mereka menghentikan program. Sangat mudah untuk melihat bagaimana perilaku ini dapat menyebabkan kode yang lebih kuat.

JacquesB
sumber
2
@atoth Berikut ini sebuah contoh. Bayangkan bahasa A memungkinkan pengecualian. Maka Anda diberi fungsi doSomeStuff(x: Int): Int. Biasanya Anda mengharapkannya mengembalikan Int, tetapi bisakah itu memberikan pengecualian juga? Tanpa melihat kode sumbernya, Anda tidak bisa tahu. Sebaliknya, bahasa B yang mengkodekan kesalahan melalui tipe mungkin memiliki fungsi yang sama dideklarasikan seperti ini: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(dalam Elm jenis ini sebenarnya bernama Result). Tidak seperti pada kasus pertama, sekarang jelas apakah fungsi tersebut mungkin gagal, dan Anda harus menanganinya secara eksplisit.
Andres F.
1
Seperti @RobertHarvey menyiratkan dalam komentar pada jawaban lain, ini tampaknya pada dasarnya seperti pengecekan pengecualian di Jawa. Apa yang saya pelajari dari bekerja dengan Java pada hari-hari awal ketika sebagian besar pengecualian diperiksa adalah bahwa Anda benar-benar tidak ingin dipaksa untuk menulis kode untuk selalu kesalahan pada saat itu terjadi.
JimmyJames
2
@JimmyJames Ini tidak seperti pengecekan yang dicentang karena pengecualian tidak membuat, dapat diabaikan ("ditelan") dan bukan nilai-nilai kelas :) Saya sangat merekomendasikan belajar bahasa fungsional yang diketik secara statis untuk benar-benar memahami hal ini. Ini bukan hal baru yang dibuat Elm - ini adalah bagaimana Anda memprogram dalam bahasa seperti ML atau Haskell, dan ini berbeda dari Java.
Andres F.
2
@AndresF. this is how you program in languages such as ML or HaskellDi Haskell, ya; ML, tidak. Robert Harper, kontributor utama bagi ML Standar dan peneliti bahasa pemrograman, menganggap pengecualian berguna . Jenis kesalahan dapat menghalangi komposisi fungsi dalam kasus di mana Anda dapat menjamin kesalahan tidak akan terjadi. Pengecualian juga memiliki kinerja yang berbeda. Anda tidak membayar untuk pengecualian yang tidak dilemparkan, tetapi Anda membayar untuk memeriksa nilai kesalahan setiap waktu, dan pengecualian adalah cara alami untuk mengekspresikan backtracking dalam beberapa algoritma
Doval
2
@JimmyJames Semoga Anda melihat sekarang bahwa pengecualian diperiksa dan jenis kesalahan yang sebenarnya hanya serupa secara dangkal. Pengecualian yang dicentang tidak menggabungkan dengan anggun, tidak praktis untuk digunakan, dan tidak berorientasi pada ekspresi (dan karena itu Anda dapat "menelan "nya, seperti yang terjadi pada Java). Pengecualian yang tidak diperiksa tidak terlalu merepotkan, itulah sebabnya mereka menjadi norma di mana-mana kecuali di Jawa, tetapi mereka lebih cenderung membuat Anda tersandung, dan Anda tidak dapat mengetahui dengan melihat pada deklarasi fungsi apakah melempar atau tidak, membuat program Anda lebih sulit untuk mengerti.
Andres F.
2

Pertama, harap perhatikan bahwa contoh pengecualian "menelan" pada umumnya adalah praktik yang mengerikan dan sama sekali tidak terkait dengan tidak memiliki pengecualian waktu berjalan; ketika Anda memikirkannya, Anda memang memiliki kesalahan run time, tetapi Anda memilih untuk menyembunyikannya dan tidak melakukan apa-apa. Ini sering menghasilkan bug yang sulit dimengerti.

Pertanyaan ini dapat ditafsirkan dalam beberapa cara, tetapi karena Anda menyebutkan Elm dalam komentar, konteksnya lebih jelas.

Elm, antara lain, adalah bahasa pemrograman yang diketik secara statis . Salah satu manfaat dari sistem jenis ini adalah bahwa banyak kelas kesalahan (meskipun tidak semua) ditangkap oleh kompiler, sebelum program tersebut benar-benar digunakan. Beberapa jenis kesalahan dapat dikodekan dalam jenis (seperti Elm Resultdan Task), bukannya dibuang sebagai pengecualian. Inilah yang dimaksudkan oleh para desainer Elm: banyak kesalahan akan ditangkap pada waktu kompilasi alih-alih pada "run time", dan kompiler akan memaksa Anda untuk menghadapinya alih-alih mengabaikannya dan berharap yang terbaik. Sudah jelas mengapa ini merupakan keuntungan: lebih baik programmer mengetahui masalah sebelum pengguna melakukannya.

Perhatikan bahwa saat Anda tidak menggunakan pengecualian, kesalahan dikodekan dengan cara lain yang tidak terlalu mengejutkan. Dari dokumentasi Elm :

Salah satu jaminan Elm adalah bahwa Anda tidak akan melihat kesalahan runtime dalam praktiknya. NoRedInk telah menggunakan Elm dalam produksi selama sekitar satu tahun sekarang, dan mereka masih belum memilikinya! Seperti semua jaminan di Elm, ini tergantung pada pilihan desain bahasa yang mendasar. Dalam hal ini, kami terbantu oleh fakta bahwa Elm memperlakukan kesalahan sebagai data. (Pernahkah Anda memperhatikan kami membuat banyak hal di sini?)

Desainer Elm agak berani mengklaim "tidak ada pengecualian waktu berjalan" , meskipun mereka memenuhi syarat dengan "dalam praktik". Apa yang mereka maksudkan adalah "kesalahan yang lebih tidak terduga daripada jika Anda mengkode dalam javascript".

Andres F.
sumber
Apakah saya salah membaca, atau mereka hanya memainkan permainan semantik? Mereka melarang nama "run time exception", tetapi kemudian menggantinya dengan mekanisme berbeda yang membawa informasi kesalahan ke stack. Ini terdengar seperti hanya mengubah implementasi pengecualian ke konsep atau objek yang serupa yang mengimplementasikan pesan kesalahan secara berbeda. Itu hampir tidak menghancurkan bumi. Ini seperti bahasa yang diketik secara statis. Bandingkan beralih dari COM HRESULT ke pengecualian .NET. Mekanisme berbeda, tetapi masih merupakan pengecualian waktu berjalan, apa pun yang Anda sebut itu.
Mike Mendukung Monica
@ Mike Sejujurnya saya belum melihat Elm secara detail. Dilihat oleh dokumen, mereka memiliki jenis Resultdan Taskyang terlihat sangat mirip dengan yang lebih akrab Eitherdan Futuredari bahasa lain. Tidak seperti pengecualian, nilai dari tipe ini dapat digabungkan dan pada titik tertentu Anda harus menanganinya secara eksplisit: apakah mereka mewakili nilai yang valid atau kesalahan? Saya tidak membaca pikiran, tetapi kurangnya mengejutkan programmer mungkin adalah apa yang dimaksud oleh desainer Elm dengan "tidak ada pengecualian waktu berjalan" :)
Andres F.
@ Mike Saya setuju itu tidak menghancurkan bumi. Perbedaannya dengan pengecualian runtime adalah bahwa mereka tidak eksplisit dalam jenis (yaitu Anda tidak bisa tahu, tanpa melihat kode sumbernya, apakah sepotong kode dapat melempar); kesalahan pengkodean dalam jenis sangat eksplisit dan mencegah programmer dari menghadapinya, mengarah ke kode yang lebih aman. Ini dilakukan oleh banyak bahasa FP dan memang bukan hal yang baru.
Andres F.
1
Sesuai komentar Anda, saya pikir ada lebih dari "pemeriksaan tipe statis" untuk itu. Anda bisa menambahkannya ke JS menggunakan Typcript, yang jauh lebih sedikit membatasi daripada ekosistem "make-or-break" baru.
atoth
1
@AndresF .: Secara teknis, fitur terbaru dari sistem tipe Java adalah Parametric Polymorphism, yang berasal dari akhir 60-an. Jadi, dengan cara berbicara, mengatakan "modern" ketika yang Anda maksud "bukan Java" agak benar.
Jörg W Mittag
0

Elm mengklaim:

Dalam praktiknya tidak ada kesalahan runtime . Tidak nol. Tidak terdefinisi bukan fungsi.

Tetapi Anda bertanya tentang pengecualian runtime . Ada perbedaan.

Di Elm, tidak ada yang mengembalikan hasil yang tidak terduga. Anda TIDAK bisa menulis program yang valid di Elm yang menghasilkan kesalahan runtime. Dengan demikian, Anda tidak perlu pengecualian.

Jadi, pertanyaannya adalah:

Apa manfaat memiliki "tidak ada kesalahan runtime"?

Jika Anda dapat menulis kode yang tidak pernah memiliki kesalahan runtime, program Anda tidak akan pernah macet.

Menggertak
sumber