1 poin oleh GN⁺ 5 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • 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: unwrap dan clone tidak memunculkan peringatan, todo! juga bergantung pada peringatan tidak langsung, sedangkan trik /// dapat berujung pada error kompilasi atau dokumentasi yang tidak disengaja tergantung posisinya
  • Crate wip melalui wip!, unwrap_wip, clone_wip, dan fixme! membuat implementasi sementara, unwrap/clone sementara, serta catatan untuk diperbaiki nanti selalu muncul sebagai peringatan
  • Meilisearch menggunakan alur yang mengubah peringatan menjadi error dengan -D warning di CI untuk mencegah kode WIP masuk, lalu menghapus dependensi wip yang 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 rustc memblokir kompilasi karena hal seperti pengembalian Result, 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 warning untuk 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 unwrap pada Result atau Option memungkinkan mengambil nilai di dalamnya dan melanjutkan implementasi saat ini, dengan konsekuensi panic pada jalur error
    • Setelah sisa kode siap, unwrap yang 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
  • 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-clone untuk 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
  • Peringatan compiler bawaan dan trik komentar dokumentasi

    • Peringatan bawaan compiler Rust juga berguna untuk menampakkan status belum selesai selama pengembangan
      • Binding yang tidak digunakan
      • mut yang tidak diperlukan
      • Result dan Option yang 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

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 terjadi
  • unwrap dan clone sama sekali tidak memunculkan peringatan, sehingga harus dibaca ulang dan dihapus sebelum PR final
  • unwrap juga 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 wip adalah 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::*;
  • wip bergantung 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 seperti todo!, 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 makro std, Rust tidak menyukainya sehingga muncul masalah dalam penggunaan prelude
  • unwrap_wip dan clone_wip

    • unwrap_wip dan clone_wip diimplementasikan sebagai extension trait untuk Result dan Option
    • Jika berada dalam scope, keduanya berperilaku seperti unwrap dan clone biasa, tetapi memunculkan peringatan saat digunakan
    • Ini dapat memberi sinyal yang jelas bahwa niatnya bukan meninggalkan unwrap atau clone di kode final
  • wip::fixme!

    • Makro fixme! adalah varian non-panicking dari wip! 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

Alur penggunaan dan hal yang perlu diperhatikan

  • Crate wip telah digunakan di beberapa PR terbaru, termasuk Meilisearch PR #6484, salah satu PR besar
  • Alur umumnya adalah menambahkan dependensi wip ke crate di workspace selama pengembangan, lalu menghapusnya saat menulis ulang histori sebelum review
  • wip membantu 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 wip dapat bertambah banyak dan terasa membebani
  • API saat ini dapat dilihat di dokumentasi wip di docs.rs

1 komentar

 
GN⁺ 5 jam lalu
Komentar di Lobste.rs
  • Jujur, kalau melihat alasan di balik wip, kesannya mereka menciptakan ulang Clippy karena tidak memahami bagaimana alat ini seharusnya digunakan
    Para pengembang toolchain selama ini membedakan mana yang menjadi peringatan rustc dan mana yang menjadi lint Clippy, tetapi wip terasa seperti membuat ulang fungsi yang sudah ada dengan nama baru, lalu memaksa lint yang tadinya ada di cargo clippy pindah ke cargo check dan cargo build
    Misalnya, penjelasan wip! pada dasarnya membuat #![warn(clippy::todo)] muncul juga di luar cargo clippy, dengan biaya menambahkan dependensi baru dan sintaks nonstandar; unwrap_wip juga 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_wip dan fixme!, dan belum pernah memikirkannya cukup jauh untuk bisa menjawab bagaimana para pengembang bermaksud agar kasus semacam itu ditangani

    • Itu mungkin komentar yang jujur, tetapi bukan ungkapan yang terlalu ramah :-) Saya sudah memakai bidang ini selama 10 tahun, dan tentu saja itu bukan berarti saya tahu segalanya, tetapi rasanya masih bisa diberi penafsiran dengan itikad baik
      Namun 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 rustc jauh lebih cepat, dan bisa melihat peringatan inline saat bekerja secara iteratif menjadi inti dari workflow ini
      Konfigurasi 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 -D eksplisit 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 saya todo! semestinya secara default diperingatkan oleh rustc atau Clippy, dan saya setuju bahwa kalau itu terjadi, kegunaan wip akan berkurang
      Saya tidak setuju bahwa unwrap_wip sama dengan #![warn(clippy::unwrap_used)] dalam dua hal. Pertama, dari sisi maksud, pembedaan bahwa unwrap untuk nonproduksi dan expect untuk produksi memang umum, tetapi tidak universal. Kedua, di codebase yang ada kami punya sekitar 4.600 unwrap, dan semuanya memang bisa diubah menjadi expect, 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 pakai
      Tulisan 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 seperti todo di fungsi impl Trait pada posisi return iterator, dan wip_future juga demikian. Cukup sering kami harus memaksakan pembuatan tipe karena inferensi tipe return dan todo tidak bekerja bersama. Ada juga yang mengusulkan agar secara default menghasilkan error kompilasi di mode rilis, dan kami akan meninjaunya bersama ide perbaikan lain
      Untuk 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 unwrap adalah 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 muncul
    Untuk tipe error, saya sarankan memulai dengan Result memakai Box<dyn Error>, string, anyhow, dan sejenisnya. Mengubahnya nanti menjadi enum error sungguhan masih mungkin, tetapi merombak kode yang tampak seperti tidak bisa gagal agar mengembalikan Result di kemudian hari lebih sulit
    Selain 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 main ada satu-dua komentar yang menjelaskan di mana/kapan penanganan error yang longgar dapat diterima

  • Saya dan dokumentasi tidak sepakat soal interpretasi todo!() dan unimplemented!(). Namun paragraf berikutnya yang dihilangkan memang sedikit menyeimbangkan posisi itu
    Dokumentasi menyebut todo! menyampaikan niat untuk mengimplementasikannya nanti dan pesannya adalah “not yet implemented”, sedangkan unimplemented! 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 dengan unimplemented!
    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 merilis todo! sebagai bentuk singkat dari unimplemented!
    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 itu

  • Saya penasaran apakah alur menambahkan dependensi wip ke crate dalam workspace selama pengembangan, lalu menghapusnya dengan menulis ulang histori sebelum review, bisa dibuat lebih mulus dengan alat tambahan
    Harus 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 fix bisa disalahgunakan untuk ini. Bagaimanapun, ide proyeknya sangat bagus, dan rasanya akan saya pakai di suatu tempat

  • Makro untuk catatan yang tidak memicu panic seperti wip::fixme! benar-benar ide bagus. Trik komentar dokumentasi juga merupakan cara rapi yang perlu diingat

  • Ada beberapa ide menarik yang semoga berujung pada perubahan di sisi rustc. todo!() benar-benar seharusnya memunculkan peringatan default
    Akan 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 cargo dijalankan 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 ini
    Selain 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 cepat

  • Tidak 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 bermimpi

  • Setiap 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