Meninggalkan Kode WIP sebagai Peringatan di Rust
(blog.dureuill.net)- Saat mengembangkan Rust, membereskan semua jalur error dan masalah ownership secara langsung bisa mudah memutus alur implementasi, sehingga strategi membuat happy path terlebih dahulu lalu melacak pekerjaan tersisa sebagai peringatan berguna
- Dengan Rust standar saja, kode yang belum selesai dapat ditandai dengan mengombinasikan
unwrap,clone,todo!, peringatan compiler bawaan, dan trik komentar dokumentasi/// - Cara yang ada memiliki keterbatasan:
unwrapdanclonetidak memunculkan peringatan,todo!juga bergantung pada peringatan tidak langsung, sedangkan trik///dapat berujung pada error kompilasi atau dokumentasi yang tidak disengaja tergantung posisinya - Crate
wipmelaluiwip!,unwrap_wip,clone_wip, danfixme!membuat implementasi sementara,unwrap/clonesementara, serta catatan untuk diperbaiki nanti selalu muncul sebagai peringatan - Meilisearch menggunakan alur yang mengubah peringatan menjadi error dengan
-D warningdi CI untuk mencegah kode WIP masuk, lalu menghapus dependensiwipyang ditambahkan selama pengembangan saat merapikan histori sebelum review
Mengapa peringatan dibutuhkan saat mengembangkan Rust
- Rust adalah bahasa yang menekankan ketepatan, tetapi jika semua jalur error langsung ditangani selama pengembangan, kecepatan implementasi dan fokus bisa menurun
- Saat
rustcmemblokir kompilasi karena hal seperti pengembalianResult, penanganan error, atau implementasi edge case, biasanya ada dua pilihan- Segera mengimplementasikan penanganan error yang lengkap, lalu melakukan refactor berkali-kali sampai PR siap
- Meninggalkan penanda bernuansa
TODO later, menurunkan masalah ke level peringatan, dan melanjutkan pekerjaan saat ini
- Peringatan tidak menghambat pengembangan, tetapi tetap menjadi sinyal bahwa sesuatu harus dibereskan sebelum PR digabung
- Meilisearch menjalankan compiler di CI dengan flag
-D warninguntuk mengubah semua peringatan menjadi error, agar kode WIP tidak terlanjur dirilis karena kelalaian - Review kode adalah praktik dasar untuk mencegah bug dan berbagi pengetahuan dalam tim, tetapi jika hanya mengandalkan ingatan dan perhatian manusia, kesalahan pada akhirnya bisa terjadi sehingga dibutuhkan guardrail tambahan
Cara menunda penanganan kode yang belum selesai dengan Rust standar
-
Menunda penanganan error dengan
unwrap- Menggunakan
unwrappadaResultatauOptionmemungkinkan mengambil nilai di dalamnya dan melanjutkan implementasi saat ini, dengan konsekuensi panic pada jalur error - Setelah sisa kode siap,
unwrapyang ditambahkan sementara dapat dipilih, dihapus, dan diganti dengan penanganan error yang tepat - Cara ini berangkat dari asumsi bahwa meskipun Rust menyediakan struktur untuk menangani error dengan benar, penanganan tersebut tidak harus diselesaikan seketika
- Menggunakan
-
Menunda masalah ownership dengan
clone- Masalah ownership lebih jarang dibanding penanganan error, tetapi menjaga ownership yang benar bisa menuntut perubahan desain
- Pada kondisi seperti ini, alih-alih memindahkan nilai, nilai dapat di-
cloneuntuk sementara menghindari masalah lifetime - Nantinya, berdasarkan pengalaman, kasus ketika clone dapat dihapus dengan meneruskan variabel yang tepat ke stack yang lebih tinggi dapat diidentifikasi
- Namun, karena perbaikannya bisa berujung pada perubahan struktur kode, menundanya lebih rumit dibanding penanganan error
-
Menyediakan tempat implementasi dengan
todo!()- Makro
todo!()dari standard library Rust menunjukkan kode yang belum selesai, dan berguna sebagai placeholder agar analisis tipe lolos saat prototyping - Karena
todo!()diverge melalui panic, makro ini dapat digunakan sebagai ekspresi placeholder di sebagian besar posisi tipe - Saat merancang interface, definisi fungsi bisa tetap digunakan sementara implementasi sebenarnya ditunda
todo!()lebih merupakan fitur praktis WIP bagi penulis kode dan reviewer, bukan untuk memberi tahu pengguna program bahwa fitur belum diimplementasikan- Untuk memberi tahu pengguna bahwa suatu fitur belum diimplementasikan, tersedia makro
unimplemented!()secara terpisah
- Makro
-
Peringatan compiler bawaan dan trik komentar dokumentasi
- Peringatan bawaan compiler Rust juga berguna untuk menampakkan status belum selesai selama pengembangan
- Binding yang tidak digunakan
mutyang tidak diperlukanResultdanOptionyang tidak digunakan
- Jika selama pengembangan peringatan seperti ini dihapus sementara dengan prefiks
_atau trik lain, ada risiko kode yang belum selesai lolos begitu saja - Edge case atau logika rusak yang tidak dilacak oleh type system kadang ditinggalkan sebagai komentar
- Jika komentar dibuat terlihat seperti komentar dokumentasi
///, Rust akan memunculkan peringatan untuk komentar dokumentasi yang tidak memiliki target dokumentasi, sehingga masalah di lokasi tersebut dapat dilacak sebagai peringatan
- Peringatan bawaan compiler Rust juga berguna untuk menampakkan status belum selesai selama pengembangan
Keterbatasan cara standar
- Masalah terbesar adalah semua teknik tersebut tidak secara andal membuat peringatan yang terkait dengan masalah yang harus diselesaikan
todo!()sering memicu peringatan, tetapi banyak yang berasal dari penyebab tidak langsung, seperti argumen fungsi yang tidak digunakan atau mutation yang diperlukan belum terjadiunwrapdanclonesama sekali tidak memunculkan peringatan, sehingga harus dibaca ulang dan dihapus sebelum PR finalunwrapjuga bisa saja secara sah tetap ada di produksi, sehingga penggunaan sementara untuk WIP dan penggunaan normal dapat bercampur- Trik komentar dokumentasi
///lebih mirip hack, dan pada posisi ekspresi komentar dokumentasi dilarang sehingga bisa menyebabkan error kompilasi - Jika
///digunakan di posisi yang benar-benar terdokumentasikan, peringatan yang diharapkan tidak muncul, dan dokumentasi yang tidak diinginkan dapat tertinggal di kode rilis
Alat yang disediakan crate wip
- Crate
wipadalah kumpulan alat untuk membuat strategi “peringatan untuk ditangani nanti” lebih nyaman selama pengembangan Rust - Dapat ditambahkan seperti crate biasa
cargo add wip
- Pada file yang membutuhkan, prelude dapat diimpor
use wip::prelude::*;
wipbergantung pada deprecation warning Rust, sehingga fitur editor yang memberi coretan pada pemanggilan fungsi deprecated mungkin sebaiknya dimatikan agar tidak terlalu mengganggu-
wip::wip!- Makro
wip!berperilaku sepertitodo!, tetapi selalu memunculkan peringatan saat digunakan - Dapat digunakan di tempat yang sebelumnya memakai
todo!untuk menandai kode yang belum selesai - Pada versi awal, namanya adalah
todo, tetapi ketika dependensi mencoba men-shadow makrostd, Rust tidak menyukainya sehingga muncul masalah dalam penggunaan prelude
- Makro
-
unwrap_wipdanclone_wipunwrap_wipdanclone_wipdiimplementasikan sebagai extension trait untukResultdanOption- Jika berada dalam scope, keduanya berperilaku seperti
unwrapdanclonebiasa, tetapi memunculkan peringatan saat digunakan - Ini dapat memberi sinyal yang jelas bahwa niatnya bukan meninggalkan
unwrapatauclonedi kode final
-
wip::fixme!- Makro
fixme!adalah varian non-panicking dariwip!dan tidak menggantikan ekspresi - Dapat digunakan seperti komentar TODO, tetapi menghasilkan peringatan secara stabil tanpa trik
/// - Sering digunakan untuk meninggalkan catatan yang harus diperbaiki nanti
- Makro
Alur penggunaan dan hal yang perlu diperhatikan
- Crate
wiptelah digunakan di beberapa PR terbaru, termasuk Meilisearch PR #6484, salah satu PR besar - Alur umumnya adalah menambahkan dependensi
wipke crate di workspace selama pengembangan, lalu menghapusnya saat menulis ulang histori sebelum review wipmembantu berfokus pada happy path sambil memastikan pekerjaan yang harus ditangani nanti tidak terlupakan- Ada juga kekurangannya
- Jika konteks yang cukup tidak ditulis saat menambahkan
fixme, perbaikannya nanti bisa sulit dilakukan - Pada PR besar, jumlah peringatan yang dibuat oleh
wipdapat bertambah banyak dan terasa membebani
- Jika konteks yang cukup tidak ditulis saat menambahkan
- API saat ini dapat dilihat di dokumentasi
wipdi docs.rs
1 komentar
Komentar di Lobste.rs
Jujur, kalau melihat alasan di balik
wip, kesannya mereka menciptakan ulang Clippy karena tidak memahami bagaimana alat ini seharusnya digunakanPara pengembang toolchain selama ini membedakan mana yang menjadi peringatan
rustcdan mana yang menjadi lint Clippy, tetapiwipterasa seperti membuat ulang fungsi yang sudah ada dengan nama baru, lalu memaksa lint yang tadinya ada dicargo clippypindah kecargo checkdancargo buildMisalnya, penjelasan
wip!pada dasarnya membuat#![warn(clippy::todo)]muncul juga di luarcargo clippy, dengan biaya menambahkan dependensi baru dan sintaks nonstandar;unwrap_wipjuga mirip dengan#![warn(clippy::unwrap_used)]Bagi saya,
.unwrap()adalah untuk prototyping, jadi saya mengaktifkan#![warn(clippy::unwrap_used)], sementara.expect("description of invariant")adalah untuk produksi, ketika orang mungkin memakai.unwrap()alih-alih.unwrap_wip(), jadi saya tidak mengaktifkan#![warn(clippy::expect_used)]. Contoh:.expect("parse regex from const")Saya tidak begitu yakin soal
clone_wipdanfixme!, dan belum pernah memikirkannya cukup jauh untuk bisa menjawab bagaimana para pengembang bermaksud agar kasus semacam itu ditanganiNamun memang benar bahwa tulisan itu melewatkan pembahasan tentang linter yang sudah ada seperti Clippy. Ini juga terkait dengan cara pakai pribadi saya: dulu saya menjalankan Clippy saat menyimpan, tetapi di proyek kami terlalu lambat sampai memakan beberapa menit di antara penyimpanan, jadi saya berhenti melakukannya; sekarang kami hanya menjalankannya di CI dan hook sebelum push
Membawa peringatan ke
rustcjauh lebih cepat, dan bisa melihat peringatan inline saat bekerja secara iteratif menjadi inti dari workflow iniKonfigurasi Clippy juga, setidaknya terakhir kali saya cek, cukup merepotkan. Tidak ada cara untuk menetapkan lint sekaligus sebagai kode untuk semua crate dalam workspace, jadi untuk setiap lint yang ingin ditolak di workspace, kami harus menambahkan
-Deksplisit di CI atau terus menaruh atribut global di semua crate#![warn(clippy::todo)]diperlukan di semua crate, jadi bagi kami itu bukan keuntungan besar dibanding menambah/menghapus crate hanya selama pengembangan. Menurut sayatodo!semestinya secara default diperingatkan olehrustcatau Clippy, dan saya setuju bahwa kalau itu terjadi, kegunaanwipakan berkurangSaya tidak setuju bahwa
unwrap_wipsama dengan#![warn(clippy::unwrap_used)]dalam dua hal. Pertama, dari sisi maksud, pembedaan bahwaunwrapuntuk nonproduksi danexpectuntuk produksi memang umum, tetapi tidak universal. Kedua, di codebase yang ada kami punya sekitar 4.600 unwrap, dan semuanya memang bisa diubah menjadiexpect, tetapi itu berarti harus memasukkan pesan sementara di banyak tempat atau menghabiskan terlalu banyak waktu untuk menyusun ulang prasyarat seperti “mutex ini tidak di-poison” atau “saya ingin meneruskan kegagalan operasi ini ke pemanggil”fixme!sangat berguna, dan saya berharap itu masuk ke standard library atau semacamnya. Di crate ini, itu benar-benar fitur yang paling sering kami pakaiTulisan tersebut tidak membahas detail-detail ini, tetapi karena crate ini bukan standar melainkan library khusus, ada juga fitur seperti
wip_iter. Fitur itu memungkinkan penggunaan makro sepertitododi fungsiimpl Traitpada posisi return iterator, danwip_futurejuga demikian. Cukup sering kami harus memaksakan pembuatan tipe karena inferensi tipe return dantodotidak bekerja bersama. Ada juga yang mengusulkan agar secara default menghasilkan error kompilasi di mode rilis, dan kami akan meninjaunya bersama ide perbaikan lainUntuk sedikit menyeimbangkan biaya berupa dependensi baru dan sintaks nonstandar: ini adalah dependensi satu file yang hanya dipakai selama pengembangan dan biasanya dihapus ketika selesai
Masalah saat prototyping dengan
unwrapadalah ketika nanti ingin memasukkan error dengan benar, bentuk tipe return bisa berubah, dan terutama jika fungsi-fungsi itu dipakai dengan gaya fungsional, efek berantainya bisa munculUntuk tipe error, saya sarankan memulai dengan
ResultmemakaiBox<dyn Error>, string,anyhow, dan sejenisnya. Mengubahnya nanti menjadi enum error sungguhan masih mungkin, tetapi merombak kode yang tampak seperti tidak bisa gagal agar mengembalikanResultdi kemudian hari lebih sulitSelain itu, jika memakai LLM, perlu berhati-hati. LLM adalah mesin pencocok pola, jadi tidak bisa membedakan antara “ini sengaja dibuat asal dulu dan ingin diperbaiki nanti” dengan “proyek ini memang harus menangani error seperti ini”. Seiring waktu, keputusan “untuk sekarang saja” bisa berkembang biak, jadi akan membantu jika di
mainada satu-dua komentar yang menjelaskan di mana/kapan penanganan error yang longgar dapat diterimaSaya dan dokumentasi tidak sepakat soal interpretasi
todo!()danunimplemented!(). Namun paragraf berikutnya yang dihilangkan memang sedikit menyeimbangkan posisi ituDokumentasi menyebut
todo!menyampaikan niat untuk mengimplementasikannya nanti dan pesannya adalah “not yet implemented”, sedangkanunimplemented!tidak memberi janji seperti itu dan pesannya adalah “not implemented”Ketika
todo!ditambahkan di 1.40.0, itu semata-mata nama yang lebih pendek, dan selain nama tidak ada bedanya denganunimplemented!Baru di 1.42.0 kata “yet” dihapus dari pesan
unimplemented. Seingat saya, penetapan makna seperti ini untuk kedua makro tersebut juga agak kontroversial, begitu pula keputusan merilistodo!sebagai bentuk singkat dariunimplemented!Saat ini
unimplemented!()direkomendasikan untuk kasus seperti implementasi trait yang secara teknis harus menyediakan isi metode, tetapi tidak boleh pernah dipanggil. Contoh di dokumentasi juga persis berbentuk seperti ituSaya penasaran apakah alur menambahkan dependensi
wipke crate dalam workspace selama pengembangan, lalu menghapusnya dengan menulis ulang histori sebelum review, bisa dibuat lebih mulus dengan alat tambahanHarus memperbaiki bagian tengah PR sekaligus menulis ulang lock file terasa agak kurang menyenangkan. Jika sementara itu dependency tree juga berubah di tempat lain, konflik yang tidak perlu bisa muncul. Mungkin
jj fixbisa disalahgunakan untuk ini. Bagaimanapun, ide proyeknya sangat bagus, dan rasanya akan saya pakai di suatu tempatMakro untuk catatan yang tidak memicu panic seperti
wip::fixme!benar-benar ide bagus. Trik komentar dokumentasi juga merupakan cara rapi yang perlu diingatAda beberapa ide menarik yang semoga berujung pada perubahan di sisi
rustc.todo!()benar-benar seharusnya memunculkan peringatan defaultAkan lebih baik lagi jika pengembang bisa mematikan peringatan seperti ini saat sedang dalam mode hacking. Kalau peringatannya terlalu banyak, itu bisa terasa membanjiri, dan biasanya
cargodijalankan di konsol 19 baris di bagian bawah IDE. Selama pengembangan, peringatan dapat menenggelamkan error sungguhan dan peringatan yang lebih penting. Sekadar disiplin untuk selalu mengurutkan error di bagian terakhir saja sudah bisa meringankan masalah iniSelain itu, nama
.unwrap_wip()dan.clone_wip()lebih panjang dari nama aslinya, jadi terlalu panjang. Saya menyarankan nama seperti.du()dan.dc()sebagai singkatan dari “dev unwrap” dan “dev clone”. Itu akan lebih mudah dipakai dalam pemrograman cepatTidak bisakah
.clone()dan.unwrap()ditambahkan secara otomatis di tempat yang diperlukan? Misalnya dengan flag atau fitur seperti#[wip(autoclone,autounwrap)]. Mungkin tidak bisa, tetapi boleh saja bermimpiSetiap kali memulai proyek baru atau prototipe, saya memasukkan baris berikut ke
main.rs:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]Ini mengurangi banyak noise pada tahap awal