Menu

Raises dan Check di Zero: Error Eksplisit Tanpa Exception

Fungsi Zero menyatakan cara mereka bisa gagal dengan raises, dan pemanggil mengakuinya dengan check. Inilah cara kerja sistemnya, kenapa tidak ada throw diam-diam, dan bagaimana sistem ini berinteraksi dengan capability World.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

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:

  • raises di signature fungsi: "fungsi ini bisa gagal."
  • check di 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:

  • "validate mengembalikan i32 atau melempar InvalidInput."
  • "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:

  1. Memanggil validate(true).
  2. Kalau validate melempar error, error itu diteruskan ke atas: run melempar 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 mencantumkan Empty dan Malformed (atau himpunan yang lebih besar).
  • Menangani salah satu atau kedua error secara eksplisit dengan match atau konstruksi bergaya try yang 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:

KonsepTry/CatchZero
Menandai fungsi sebagai bisa gagaltidak ada (implisit)raises { ... }
Melempar errorthrow eraise E
Meneruskan ke pemanggilnaik tanpa terlihatcheck call(...)
Menangani di tempattry { ... } 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 check sesering yang kamu perlukan. Itu default yang tepat saat tidak ada hal khusus yang perlu dilakukan di lapisan ini.
  • Hindari raises saja 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 World untuk menulis ke stdout hampir selalu butuh raises, 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.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai