Idenya dalam Satu Kalimat
Program Zero melakukan I/O karena diberi izin untuk itu, bukan dengan mengambil variabel global yang selalu tersedia.
Izin itu adalah nilai bertipe World. Runtime membuatnya sebelum memanggil main, dan programmu meneruskannya (atau bagian-bagiannya) ke mana pun program perlu berinteraksi dengan dunia luar.
Kenapa Tanpa Variabel Global?
Sebagian besar bahasa membolehkan fungsi apa pun, di mana pun, menulis ke stdout atau membuka file. JavaScript punya console.log. Python punya print. C punya printf. Kemudahannya nyata, tetapi biayanya juga nyata: kamu tidak bisa tahu dari signature sebuah fungsi apakah fungsi itu mungkin melakukan I/O. Untuk tahu, kamu harus membaca isinya, dan isi fungsi yang dipanggilnya, dan seterusnya.
Zero mengambil sikap yang berbeda. Tidak ada print global. Tidak ada os.Stdout yang selalu tersedia. Kalau fungsimu melakukan I/O, fakta itu harus terlihat di signature-nya, karena satu-satunya cara melakukan I/O adalah dengan diberi sebuah capability.
Manfaatnya terlihat di tiga tempat:
- Membaca signature memberi tahu apa yang bisa dilakukan sebuah fungsi. Fungsi yang tidak menyebut
Worldtidak bisa menulis ke stdout, tidak bisa membuka socket, dan tidak bisa membaca file. Sistem tipe menjadikannya jaminan yang pasti. - Menguji kode murni jadi sangat mudah. Fungsi murni tidak butuh
printtiruan atau file system palsu, karena sejak awal fungsi itu memang tidak punya akses ke sana. - Agen bisa menalar secara lokal. Agen AI yang membuat atau memperbaiki kode Zero bisa tahu, tanpa membaca seluruh codebase, apakah fungsi yang sedang dilihatnya punya efek.
Pemakaian Standar
Kamu sudah melihat bentuk dasarnya di hello world:
Ada tiga hal yang terjadi:
mainmendeklarasikan parameternya sebagaiworld: World. Runtime memberikan nilaiWorldke program dan mengikatnya di sini.world.outadalah stream output standar, tersedia sebagai field dari capabilityWorld.world.out.write(...)menulis sebuah string. Pemanggilan ini mengembalikan nilai yang bisa gagal (penulisannya bisa gagal), dancheckmeneruskan kegagalan itu.
Kamu juga bisa mengganti nama parameternya, misalnya w: World atau io: World, tetapi world adalah konvensinya dan sebaiknya dipertahankan agar konsisten dengan ekosistem Zero secara umum.
Apa Saja yang Ada di World
Capability World menyediakan bagian-bagian yang mungkin dibutuhkan sebuah program. Bentuk persisnya tergantung apa yang didukung runtime, tetapi kamu bisa mengharapkan bagian untuk:
world.out: output standar.world.err: error standar.world.in: input standar.- Cara untuk membuka file, membaca environment variable, dan terhubung ke jaringan.
Lihat dokumentasi standard library Zero terbaru untuk daftar field yang resmi. Beberapa bagian (jaringan, filesystem) mungkin berada di balik tipe capability yang lebih sempit dan diakses lewat World, bukan di tingkat atas.
Meneruskan Capability di Dalam Kode
Ada konsekuensinya, yaitu harga yang kamu bayar untuk efek yang eksplisit: setiap fungsi yang perlu melakukan I/O harus menerima capability itu. Kamu tidak bisa mengambilnya secara implisit.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world, "starting\n")
log(world, "done\n")
}
log menerima world agar bisa menulis lewat nilai itu. Kalau log tidak menerima world, isi fungsinya tidak bisa memanggil world.out.write, karena binding-nya tidak ada.
Parameter yang harus diteruskan memang lebih banyak daripada di bahasa dengan I/O yang selalu tersedia. Gantinya, seluruh graf pemanggilan untuk main kini terlihat hanya dari signature:
mainmenerimaworld, jadi mungkin melakukan I/O.logmenerimaworld, jadi mungkin melakukan I/O.- Fungsi apa pun yang signature-nya tidak memuat
worldtidak bisa melakukannya.
Capability yang Lebih Sempit
Mengirim seluruh World ke setiap fungsi adalah pendekatan yang kasar, mirip seperti membagikan hak akses root. Pola yang dianjurkan Zero adalah hanya menerima bagian World yang benar-benar kamu butuhkan:
fun log(out: Stream, message: String) -> Void raises {
check out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world.out, "starting\n")
log(world.out, "done\n")
}
Sekarang log hanya mendapat akses ke sebuah Stream (tipe yang sama dengan yang disediakan world.out). Fungsi itu bisa menulis lewat stream tersebut, tetapi tidak bisa membuka file atau membaca dari jaringan. Pemanggilnya yang memilih apa yang boleh dilakukan log.
Nama tipe persis yang akan kamu lihat di kode Zero sungguhan (Stream, Writer, potongan capability) akan mengikuti kosakata standard library di versi toolchain kamu. Polanya, yaitu kirim yang paling sedikit, bukan yang paling banyak, berlaku di mana saja.
Fungsi Murni
Fungsi yang tidak butuh World sebaiknya tidak menerima World. Ini sebagian soal gaya, dan sebagian lagi dipaksakan oleh sistem tipe: tidak ada cara melakukan I/O tanpa capability.
fun sum(point: Point) -> i32 {
return point.x + point.y
}
sum bersifat murni terhadap dunia luar. Pemanggil yang melihat signature-nya tahu pasti bahwa fungsi ini tidak akan menampilkan apa pun, tidak akan membuka file, dan tidak akan menghubungi server. Static analyzer (atau agen) bisa mengandalkan sifat ini tanpa membaca isi fungsinya.
Capability dan raises
Hampir setiap operasi pada capability bisa gagal. world.out.write bisa gagal karena stream-nya sudah tertutup. Membuka file bisa gagal karena file-nya tidak ada. API capability dipasangkan dengan raises dan check: operasi yang bisa gagal menyatakan jenis kegagalannya di signature, dan pemanggil mengakuinya dengan check.
Gabungan keduanya adalah inti cara Zero menangani efek:
- Apa yang bisa terjadi →
raises { ... }. - Lewat apa →
World(atau sebagian darinya). - Di mana → di mana pun capability dan
raisesterlihat.
Informasi itu sudah cukup untuk menalar efek sebuah fungsi secara tepat hanya dari signature-nya.
Catatan tentang Testing
I/O berbasis capability membuat testing mudah dengan sendirinya. Ingin menangkap output fungsi yang sedang diuji? Kirim capability out palsu yang mencatat apa yang diminta untuk ditulis. Ingin menguji fungsi yang seharusnya murni? Jangan kirim capability apa pun, karena signature tipenya tidak akan membiarkan fungsi itu menyentuh dunia luar.
Standard library bisa menyediakan harness test yang membuat capability palsu atau capability di memori untuk keperluan ini. API persisnya akan berkembang bersama bahasanya, tetapi prinsipnya (capability adalah nilai yang bisa kamu ganti) adalah kuncinya.
Berikutnya: Raises dan Check
World adalah separuh cerita tentang efek, yaitu bagian-bagian yang bisa disentuh sebuah fungsi. Separuh lainnya adalah kegagalan: saat sesuatu salah, bagaimana kegagalan itu diteruskan? Itu dibahas berikutnya di Raises dan Check.
Pertanyaan yang sering diajukan
Apa itu World di Zero?
World adalah objek capability yang disediakan runtime untuk memberi program Zero akses ke dunia luar: stdout, stdin, file, jaringan, environment variable, dan sebagainya. Runtime membuat nilai World lalu mengirimkannya ke main. Fungsi yang perlu melakukan I/O harus menerima World (atau bagian yang lebih sempit darinya). Tidak ada jalan pintas global.
Kenapa main menerima parameter World?
Zero tidak punya variabel global yang selalu tersedia. Tidak ada padanan printf, console.log, atau os.Stdout yang bisa dipanggil fungsi mana pun tanpa izin. Runtime memberikan capability World ke main, dan main (serta fungsi apa pun yang dipanggilnya) hanya bisa melakukan I/O lewat nilai itu. Dengan begitu, setiap efek terlihat di signature fungsi.
Apa bedanya I/O berbasis capability dengan I/O biasa?
Di sebagian besar bahasa, I/O bersifat implisit: fungsi apa pun bisa menulis ke stdout atau membaca dari filesystem kapan saja. I/O berbasis capability menjadikan izin untuk melakukan I/O sebagai sebuah nilai: kamu harus diberi World (atau sebagian darinya) untuk memakainya. Fungsi perhitungan murni tidak menerima World, jadi memang tidak bisa melakukan I/O, dan sistem tipe memastikan hal itu.
Bisakah saya mendapatkan World secara implisit di bagian dalam call stack?
Tidak, dan itu disengaja. Kalau sebuah fungsi bantuan jauh di dalam call stack perlu menulis ke stdout, kamu harus mengirimkan World (atau capability yang lebih sempit) kepadanya secara eksplisit. Parameter yang harus diteruskan memang lebih banyak daripada di bahasa dengan I/O yang selalu tersedia, tetapi itulah harga untuk bisa membaca signature sebuah fungsi dan tahu apakah fungsi itu mungkin melakukan I/O.
Apa yang dilakukan world.out.write?
world.out.write("text\n") menulis string yang diberikan ke stream output standar program lewat capability yang disediakan runtime. Pemanggilan ini mengembalikan nilai yang bisa gagal (penulisannya mungkin gagal), jadi kamu membungkusnya dengan check untuk meneruskan error ke atas call stack.