Gambaran Umum
Kegagalan di Zero bukan alur kontrol terpisah yang berjalan sejajar. Kegagalan adalah bagian dari alur kontrol biasa, dinyatakan di signature fungsi dan diakui di setiap tempat pemanggilan. Ada dua bagian yang membuatnya bekerja:
raisesdi signature fungsi: "fungsi ini bisa gagal."checkdi tempat pemanggilan: "kalau ini gagal, gagalkan fungsiku dengan error yang sama."
Gabungan keduanya sudah cukup untuk mengungkapkan apa yang di sebagian besar bahasa ditangani dengan try/catch atau tipe Result.
Mendeklarasikan Fungsi yang Bisa Gagal
Tambahkan raises setelah tipe kembalian:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
Ada dua cara membaca signature itu:
- "
validatemengembalikani32atau melemparInvalidInput." - "Himpunan hasil yang mungkin adalah {
i32,InvalidInput}."
Kedua cara itu benar. Compiler melacak kedua kemungkinan dan menuntut pemanggil melakukan sesuatu yang eksplisit untuk masing-masing.
Kamu juga bisa menulis klausa raises saja:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
raises (tanpa daftar error) berarti "fungsi ini bisa gagal dengan error apa pun". Pada main, ini bentuk yang lazim: program bisa keluar dengan status bukan nol kalau ada yang salah, dan runtime yang mengurus penyampaian error-nya.
Untuk fungsi yang lebih dalam di call stack, lebih baik pakai bentuk eksplisit raises { ErrorA, ErrorB } agar cara-cara gagalnya terdokumentasi di setiap tingkat.
Melempar Error
Di dalam fungsi yang bisa gagal, raise keluar dari fungsi dengan error yang diberikan:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raise InvalidInput menghasilkan error InvalidInput di baris itu. Fungsi tidak berlanjut melewati titik tersebut: kontrol kembali ke pemanggil, dan pemanggil menerima error, bukan i32. Klausa raises mencantumkan satu-satunya tipe error yang boleh dilempar fungsi ini, dan melempar error yang tidak ada di daftar adalah error kompilasi.
Meneruskan Error dengan check
Pemanggil yang memanggil fungsi yang bisa gagal harus mengakui kemungkinan gagal itu. Cara mengakui yang paling umum adalah check:
fun run() -> Void raises { InvalidInput } {
check validate(true)
}
check validate(true) melakukan dua hal:
- Memanggil
validate(true). - Kalau
validatemelempar error, error itu diteruskan ke atas:runmelempar error yang sama ke pemanggil miliknya.
Agar penerusan itu diizinkan, run harus menyatakan di klausa raises-nya sendiri bahwa ia bisa melempar InvalidInput (atau error yang sesuai). Compiler memeriksa hal ini. Kalau signature run menyatakan raises { OtherError }, penerusan itu gagal di-compile karena InvalidInput tidak ada di himpunannya.
Contoh lengkap dari sampel resmi bahasa ini. Klik Jalankan untuk melihat penerusan error berjalan dengan benar:
Tipe error ikut bersama signature fungsi sampai ke puncak call stack. raises saja pada main menerima apa pun yang mungkin dilempar run, jadi penerusannya sampai dengan aman.
Kenapa Tidak try/catch?
Disiplin desain di balik raises/check adalah kegagalan tidak pernah tersembunyi di tempat pemanggilan. Di bahasa dengan try/catch, exception bisa bergerak diam-diam melewati fungsi yang bahkan tidak tahu bahwa ia mungkin terlibat. Fungsinya terlihat murni, tetapi ada pemanggilan jauh di dalam isinya yang melempar exception, dan exception itu melintas keluar melewatinya.
Itu nyaman bagi penulis kode yang melempar exception, tetapi mahal bagi orang lain:
- Pembaca (dan agen) tidak bisa tahu dari signature apakah sebuah fungsi terlibat dalam jalur kegagalan.
- Refactoring jadi menegangkan, karena memindahkan pemanggilan ke fungsi lain bisa mengubah exception apa saja yang bisa terjadi.
- Kode pemulihan berada jauh dari tempat yang tahu apa yang harus dilakukan.
Zero membayar biayanya di depan, yaitu anotasi di setiap fungsi yang bisa gagal dan check di setiap pemanggilan yang bisa gagal, untuk mendapat sifat ini: "cara-cara gagal yang melibatkan sebuah fungsi terlihat dari signature-nya." Manusia dan agen sama-sama bisa mengandalkan sifat itu.
Kenapa Tidak Result<T, E> Saja?
Kamu bisa saja mengungkapkan hal yang sama dengan choice, yaitu tipe Result<T, E> dengan varian ok dan err. Zero juga menyediakan pola itu, dan pola itu berguna saat kegagalan adalah data yang ingin kamu periksa, simpan, atau kirim ke mana-mana.
Yang ditambahkan raises/check adalah konvensi di tingkat sintaks untuk kasus yang umum: "kalau ini gagal, gagalkan fungsiku dengan cara yang sama." Tanpanya, setiap pemanggilan akan dibungkus dengan match yang hampir selalu membungkus ulang error-nya ke dalam Result milik pemanggil. check adalah jalan pintas untuk itu, dan compiler memastikan penerusannya bertipe dengan benar.
Jadi:
Result<T, E>(sebuah choice): saat kamu ingin memeriksa atau membawa kegagalan sebagai nilai.raises+check: saat kamu hanya ingin meneruskan kegagalan ke atas call stack.
Keduanya tersedia, dan masing-masing memenuhi kebutuhan kenyamanan yang berbeda.
Beberapa Tipe Error
Sebuah fungsi bisa melempar lebih dari satu jenis error:
fun parse(input: String) -> i32 raises { Empty, Malformed } {
if std.mem.len(input) == 0 {
raise Empty
}
// ... parsing logic ...
raise Malformed
}
Pemanggil bisa:
- Meneruskan dengan
check parse(input), kalau signature-nya sendiri mencantumkanEmptydanMalformed(atau himpunan yang lebih besar). - Menangani salah satu atau kedua error secara eksplisit dengan
matchatau konstruksi bergayatryyang disediakan bahasa untuk keperluan itu.
Sintaks persis untuk penanganan yang lebih rinci (mencocokkan tipe error tertentu sambil meneruskan yang lain) adalah salah satu bagian yang mungkin berubah di Zero pra-1.0. Kontraknya, yaitu setiap tipe error yang bisa dilempar sebuah fungsi ada di signature-nya, adalah bagian yang stabil.
Model Berpikir
Sistem kegagalan Zero adalah versi yang ketat dan jujur dari apa yang dimiliki setiap bahasa imperatif:
| Konsep | Try/Catch | Zero |
|---|---|---|
| Menandai fungsi sebagai bisa gagal | tidak ada (implisit) | raises { ... } |
| Melempar error | throw e | raise E |
| Meneruskan ke pemanggil | naik tanpa terlihat | check call(...) |
| Menangani di tempat | try { ... } catch(e) { ... } | match pada Result yang dikembalikan, atau bentuk penanganan bertipe |
Perbedaan perilakunya kecil. Perbedaan anotasinya besar, dan itu disengaja. Efek bersifat eksplisit. Kegagalan adalah efek.
Catatan Gaya
- Pakai
checksesering yang kamu perlukan. Itu default yang tepat saat tidak ada hal khusus yang perlu dilakukan di lapisan ini. - Hindari
raisessaja pada fungsi bantuan internal. Makin sempit himpunan error-nya, makin berguna signature-nya. - Pasangkan operasi yang bisa gagal dengan capability yang dibutuhkannya. Fungsi yang memakai
Worlduntuk menulis ke stdout hampir selalu butuhraises, karena penulisan bisa gagal.
Berikutnya: Diagnostik JSON
raises/check bekerja bersama diagnostik compiler Zero: saat kamu menulis check terhadap fungsi yang tidak menyatakan error yang tepat, compiler memberi tahu persis apa yang salah dalam bentuk terstruktur. Dokumen berikutnya membahas diagnostik JSON, output yang bisa dibaca mesin dan dipakai agen untuk memperbaiki kode.
Pertanyaan yang sering diajukan
Apa arti raises di Zero?
raises pada signature fungsi menyatakan bahwa fungsi itu bisa gagal. raises saja mengizinkan tipe error apa pun. Bentuk spesifik seperti raises { InvalidInput } membatasi fungsi agar hanya bisa gagal dengan tipe error yang dicantumkan. Pemanggil wajib mengakui kemungkinan gagal itu, baik dengan check maupun dengan bentuk penanganan eksplisit lainnya.
Apa yang dilakukan operator check?
check expr mengevaluasi expr, dan kalau hasilnya error, error itu diteruskan ke pemanggil fungsi yang sedang berjalan. Anggap saja artinya 'jalankan ini, dan kalau gagal, gagalkan pemanggilku dengan error yang sama.' Agar klausa raises milik pemanggil mengizinkan penerusan itu, pemanggil sendiri harus menyatakan bahwa ia bisa melempar error yang sesuai.
Bagaimana cara melempar error di Zero?
Pakai raise ErrorName di dalam fungsi yang klausa raises-nya memuat error tersebut. Contoh: if ok == false { raise InvalidInput }. Fungsi berhenti di titik itu, dan error tersebut menjadi hasil fungsi, yang bisa diperiksa pemanggil dengan check.
Kenapa Zero tidak memakai try/catch?
Try/catch membuat exception bisa bergerak diam-diam melewati fungsi yang tidak menyadarinya. Desain Zero mengharuskan signature setiap fungsi mengakui cara-cara gagal yang melibatkannya. Tidak ada alur kontrol tersembunyi: kalau sebuah fungsi bisa gagal, signature-nya menyatakannya, dan setiap pemanggil harus mengakuinya secara eksplisit dengan check.
Apakah sebuah fungsi bisa melempar beberapa tipe error di Zero?
Bisa. Cantumkan semuanya di klausa raises { ... }, dipisahkan koma (atau ikuti sintaks Zero terbaru untuk himpunan error). Himpunan error yang mungkin terjadi adalah bagian dari kontrak fungsi, sama seperti tipe parameter dan tipe kembaliannya. Pemanggil bisa memakai pattern matching untuk melihat error mana yang dilempar, atau cukup meneruskannya dengan check.