Peningkatan Produktivitas Rust yang Tak Terduga
(lubeno.dev)- Rust meningkatkan produktivitas dan kemudahan pemeliharaan karena refactoring dapat dilakukan dengan percaya diri bahkan pada codebase besar berkat jaminan keamanannya yang kuat
- Compiler dapat mendeteksi bug terkait penjadwalan asinkron lebih awal dan memperkuat stabilitas dengan mencegah undefined behavior
- Bahasa seperti TypeScript sering kali baru menemukan bug asinkron di lingkungan produksi karena sistem tipenya lebih longgar
- Sistem tipe Rust secara jelas menunjukkan dampak dari perubahan kode, sehingga meningkatkan kepercayaan dan kemauan untuk bereksperimen dalam proyek yang kompleks
- Berbeda dengan Rust, Zig memiliki pemeriksaan yang lebih longgar dalam penanganan error sehingga bug akibat typo bisa terlewat dan keandalannya lebih rendah
Ringkasan dan Latar Belakang
- Backend Lubeno ditulis 100% dengan Rust, dan codebase-nya telah tumbuh hingga mencapai tahap di mana sulit memahami keseluruhannya hanya di dalam kepala
- Proyek berskala besar umumnya mengalami penurunan produktivitas karena sulit memeriksa efek samping dari perubahan
- Jaminan keamanan Rust secara jelas memberi tahu dampak saat kode diubah, sehingga mengurangi rasa takut terhadap refactoring
- Hal ini berkontribusi pada peningkatan kemudahan pemeliharaan dan produktivitas jangka panjang
- Tulisan ini dimulai dari sebuah kasus ketika compiler Rust mendeteksi bug asinkron, lalu mengeksplorasi keunggulan produktivitas Rust
Contoh Jaminan Keamanan Rust
- Situasi masalah: sebuah struct dibungkus dengan mutex untuk akses bersamaan, lalu operasi asinkron dijalankan setelah lock diperoleh
let lock = mutex.lock(); db.insert_commit(commit).await; - Penemuan masalah: rust-analyzer tidak menampilkan error, tetapi terjadi error kompilasi di file definisi router
.route("/api/git/post-receive", post(git::post_receive)) ^^^^^^^^^^^^^^^^^ error: future cannot be sent between threads safely - Analisis penyebab:
- Web framework membuat task asinkron untuk setiap koneksi HTTP, lalu scheduler memindahkan task antar-thread
- Mutex harus dilepas pada thread yang sama, dan jika thread berpindah di titik
.await, bisa terjadi undefined behavior - Compiler Rust melacak masa hidup lock dan mendeteksi kemungkinan lock dilepas di thread lain
- Cara menyelesaikan: lepaskan lock sebelum
.await - Makna pentingnya: Rust mencegah bug asinkron yang sulit direproduksi di lingkungan pengembangan pada saat compile time
Contoh Perbandingan dengan TypeScript
- Situasi masalah: terjadi bug redirect asinkron pada kode TypeScript
if (redirect) { window.location.href = redirect; } let content = await response.json(); if (content.onboardingDone) { window.location.href = "/dashboard"; } else { window.location.href = "/onboarding"; } - Penyebab masalah:
window.location.hreftidak langsung melakukan redirect, melainkan menjadwalkannya, sehingga eksekusi kode tetap berlanjut- Akibat race condition, terjadi redirect yang tidak diinginkan
- Cara menyelesaikan: tambahkan
returnpada blokifif (redirect) { window.location.href = redirect; return; } - Keterbatasan: TypeScript tidak memiliki pelacakan lifetime atau aturan peminjaman, sehingga bug seperti ini tidak bisa dideteksi pada saat compile time
- Bug baru ditemukan di lingkungan produksi dan membutuhkan waktu lama untuk debugging
Keunggulan Refactoring di Rust
- Dalam pengembangan web, Python, Ruby, dan JavaScript/Node.js memiliki produktivitas awal yang tinggi, tetapi ketika codebase membesar, perubahan menjadi sulit karena keterikatan yang longgar
- Setelah perubahan, error tak terduga bisa muncul dan menurunkan kemauan untuk memperbaiki kode
- Rust membuat sistem tipe secara jelas menunjukkan dampak perubahan, sehingga mengurangi rasa takut terhadap refactoring
- Contoh: peringatan seperti “perubahan ini bisa memengaruhi bagian lain” membantu mencegah masalah lebih awal
- Bahkan ketika codebase terus tumbuh, produktivitas meningkat, kode lama dapat digunakan kembali, dan stabilitas tetap terjaga saat melakukan perubahan
Perbandingan dengan Pengujian
- Pengujian berguna untuk mencegah regresi saat refactoring, tetapi karena tidak dipaksakan oleh compiler, pengujian bisa saja dilewati
- Menulis pengujian menimbulkan beban mental karena harus menentukan tingkat abstraksi, perilaku vs detail implementasi, dan apakah pengujian benar-benar mencegah kesalahan
- Rust membuat compiler memblokir kesalahan umum sejak awal, sehingga mengurangi beban pengambilan keputusan yang biasanya ditanggung pengujian
- Sifat yang tidak dapat diverifikasi oleh sistem tipe dapat dilengkapi dengan pengujian
Perbandingan dengan Zig
- Zig adalah bahasa system programming yang mirip dengan Rust, tetapi lebih longgar dalam penanganan error
- Contoh: kode penanganan error
const FileError = error{ AccessDenied }; fn doSomethingThatFails() FileError!void { return FileError.AccessDenied; } pub fn main() !void { doSomethingThatFails() catch |err| { if (err == error.AccessDenid) { std.debug.print("Access was denied!\n", .{}); } }; } - Karena typo
AccessDenid, bug pun terjadi, tetapi compiler Zig memperlakukannya sebagai angka sehingga kompilasi tetap berhasil
- Contoh: kode penanganan error
- Saat menggunakan pernyataan switch, typo dapat dideteksi, tetapi pada pernyataan if diabaikan, sehingga menimbulkan masalah keandalan
- Rust mencegah celah desain seperti ini dan memeriksa typo maupun kesalahan logika dengan ketat
Implikasi
- Rust meningkatkan produktivitas dan stabilitas proyek besar melalui jaminan keamanan dan sistem tipe yang ketat
- Masalah kompleks seperti bug asinkron juga dapat dideteksi pada saat compile time, sehingga biaya pemeliharaan berkurang
- Contoh TypeScript dan Zig menunjukkan risiko dari pemeriksaan yang longgar, sekaligus menegaskan nilai compiler Rust yang ketat
- Rust bukan hanya unggul dalam produktivitas awal untuk pengembangan web, tetapi juga menjadi alat yang kuat untuk pengelolaan codebase jangka panjang
3 komentar
Setiap kali melihat orang bilang ini yang terbaik, ini bahasa yang kuat banget!!
Saya malah jadi berpikir, jangan-jangan karena jumlah developer Rust ternyata tidak sebanyak yang dibayangkan, jadi mereka mengiming-imingi orang supaya pakai Rust??
Artikel rekomendasi terkait Rust terasa seperti sikap seorang gourmet yang berkata, "Coba! Coba!" — mungkin cuma saya yang merasa begitu?
Komentar Hacker News
Tahun lalu saya mem-port driver jaringan virtio-host yang ditulis dengan Rust. Saya mengganti backend, mekanisme interrupt, dan mengubahnya dari library menjadi proses mandiri. Programnya cukup kompleks, menangani memory mapping, interrupt VM, socket jaringan, sampai multithreading. Pengalaman saya dengan Rust hampir tidak ada, dan pengalaman dengan virtio juga minim, tetapi saat proyeknya berhasil dikompilasi, hasilnya langsung berjalan dengan sempurna. Selain satu bug terkait Drop yang mudah diperbaiki. Saya rasa ini sangat terbantu karena library-library Rust dirancang dengan struktur yang sulit disalahgunakan
Saya pikir Rust itu hebat. Tapi saya tidak setuju dengan pendapat bahwa bug assignment
hrefitu salah TypeScript. Inti masalahnya adalah meskipunhrefdisetel, perpindahan halaman tidak terjadi seketika melainkan diproses belakangan. Masalah yang sama bisa saja terjadi di Rust. Kalau di Rust ada fungsiset_hrefdan perilaku ini diproses belakangan, maka kode seperti ini tetap mungkin terjadi:set_href('/foo')if (some_condition) {set_href('/bar')}Menurut saya, di Rust API seperti ini tidak akan didesain seperti itu. Tindakan yang terjadi dari setter bukanlah desain library yang baik, dan aneh juga kalau assignment
hreftidak langsung memindahkan halaman. Kalau ini standard library Rust, implementasi konyol seperti ini tidak akan ada. Jadi ini bukan soal Rust vs TypeScript, melainkan perbedaan antara standard library Rust dan Web Platform API. Saya setuju bahwa Rust tidak akan memberikan pengalaman pengguna seperti iniSecara formal, merancang setter agar langsung memicu aksi memang tidak ideal. Penamaannya pun seharusnya diubah menjadi sesuatu seperti
navigate_to(href). Di lingkungan browser, semua kode JS berjalan lewat callback dan dikendalikan oleh event loop, jadi wajar juga kalau tidak dieksekusi secara instanContoh Rust itu menarik, tetapi dari contoh TypeScript saja kita tidak bisa menyimpulkan apakah TS cocok untuk proyek besar. Di Ruby saya sering harus menangkap bug saat runtime sehingga terasa tidak nyaman, tetapi pada akhirnya saya tetap puas karena sebelum commit semuanya berjalan baik, dan kodenya mudah dibaca serta dimodifikasi. Isu perpindahan lokasi itu masalah JavaScript dan bagian yang diwarisi TS. Ini terjadi karena JS memungkinkan properti dimodifikasi sesuka hati. Namun halaman juga tidak langsung hilang, jadi setelah tahu perilakunya, sebenarnya itu cukup masuk akal
Secara teknis, di Rust maknanya bisa diberi petunjuk lebih jelas lewat apakah
set_hrefmengembalikan()atau!. Tapi dalam kasus redirect bersyarat, tetap sulit mencegah penyalahgunaan sepenuhnyaMaksud saya adalah bahwa dengan model ownership Rust, API bisa didesain agar saat
window.set_href('/foo')dipanggil, ownership ataswindowikut diambil sehingga pemanggilan kedua menjadi tidak mungkin. TypeScript sama sekali tidak punya konsep pelacakan lifetime, jadi hal seperti ini tidak mungkin. Karena API JS-nya sendiri sudah ada, tidak ada cara bagi TypeScript untuk memperkenalkan sistem ownership. Saya ingin menunjukkan ini sebagai contoh bagaimana berbagai fitur Rust berpadu untuk memberi jaminan yang lebih kuatDasar argumenmu bahwa Rust lebih baik akhirnya terdengar seperti “programmer Rust lebih hebat”. Rasanya programmer Rust sendiri tidak akan memakai argumen melingkar seperti itu
Kode setelah assignment tetap akan terus dieksekusi kecuali ada early return yang eksplisit. Serius, saya tidak paham kenapa ada yang mengira assignment nilai akan menghentikan eksekusi skrip. Mungkin konteks pada contoh TS kurang, tetapi menjadikannya contoh “data race” juga terasa aneh
Memberi nilai ke
window.location.hrefmemang punya efek samping berupa browser berpindah ke tautan itu. Perilaku ini cukup mengejutkan, dan karena assignment sederhana bisa memuat halaman baru, rasanya miripexecve, jadi tidak aneh kalau orang mengira eksekusi JS akan langsung berhenti. Tentu kita tidak seharusnya mengandalkan asumsi seperti itu saat memprogram, tetapi karena perilakunya memang agak aneh, saya paham kenapa orang bisa bingungTerlepas dari apakah orang memikirkannya atau tidak, bug seperti ini begitu diberi tahu seseorang akan jelas cara memperbaikinya. Inti argumen penulis adalah bahwa bug seperti ini, yang tidak bisa ditangkap TS, dalam praktiknya bisa sulit dicari dan memakan waktu lama
exit(),execve(), dan sejenisnya memang benar-benar menghentikan eksekusi saat itu juga, jadi wajar kalau orang menganggap redirect akan berperilaku samaAneh kalau orang dipermasalahkan hanya karena membagikan pengalamannya sendiri
Assignment ini punya efek samping besar, yaitu membuat halaman ditinggalkan. Jadi menganggapnya sebagai aksi asinkron yang terjadi seketika juga bukan asumsi yang terlalu aneh. Saya sendiri pernah membuat asumsi seperti itu
Ini hanya cerita bahwa seorang developer menyadari static type system itu berguna. Setiap kali melihat tulisan seperti ini selalu terasa lucu
Bukankah sebagian besar kelebihannya pada akhirnya datang dari penggunaan static typing, yaitu bahasa yang dikompilasi? Java, Go, dan C++ juga begitu. TypeScript punya trik tersendiri: ia dikompilasi ke JS dan mewarisi masalah JS, tetapi tetap cukup berguna. Rust punya sistem tipe yang lebih ketat sehingga bisa melakukan pengecekan tambahan saat compile time, tetapi sebagai gantinya lebih sulit dipelajari dan juga lebih sulit dibaca menurut saya
Saya agak setuju, tetapi Rust punya lebih banyak dimensi dalam sistem tipenya: ownership, akses bersama/eksklusif, thread safety, sum type, dan lain-lain. Berkat sistem ownership/borrowing, jadi jelas apakah argumen yang diteruskan hanyalah view sementara atau benar-benar dipindahkan sepenuhnya. Ini sangat menguntungkan untuk program besar atau saat memakai library eksternal. Misalnya, tipe slice di Go tidak selalu jelas pada runtime operasi apa yang diizinkan, dan juga kurang jelas bagaimana cara meminjamnya sebagai read-only. Rust bisa menjamin thread safety di level sistem tipe, sehingga data race yang sulit ditemukan saat runtime di bahasa lain bisa dicegah sejak compile time
Kalau semua bahasa bertipe statis dianggap satu kelompok yang sama, itu karena Anda mungkin belum benar-benar merasakan kekuatan union(sum) type dan pattern matching. Sekali sudah terbiasa dengan union type, bahasa statis tradisional lain jadi terasa kurang memuaskan
Salah satu keunggulan besar adalah
traits/impl traits. Di Rust, trait bisa ditambahkan ke tipe mana pun belakangan, mirip seperti Extension Method di C#. Di kebanyakan bahasa, tipe biasanya sudah tetap ketika didefinisikan di library, tetapi di Rust kita bisa terus menumpuk kemampuan di atas tipe sederhana secara bertahap. Sifat late-bound ini memberi unsur dinamis pada sistem tipenya. Kalau mau agak ekstrem, superpower sejati Rust bukan borrow checker, melainkan keterbukaan dan fleksibilitas sistem tipenya. Tidak perlu merancang semuanya dari awal; cukup diperluas sedikit demi sedikitTidak semua bahasa bertipe statis memberi hasil yang sama. Java pada akhirnya bergantung pada
Objectdan casting runtime. Go tidak punya enum. C++ memang menambahkan konsepvariant, tetapi untuk memakainya dengan aman tetap butuh penanganan manual semacamtry/except, sehingga secara struktural terasa kurang nyamanOrang bilang Rust sulit dipelajari, padahal kalau sudah benar-benar dipahami, sebenarnya tidak sulit. Di awal belajar ngoding, sering kali yang penting adalah bisa menulis sesuatu secara asal sampai akhirnya jalan, dan Rust memang bahasa yang kurang ramah terhadap gaya itu. Saya tidak akan merekomendasikannya sebagai bahasa pemula, tetapi untuk dibaca sebenarnya tidak sulit
Berkat safety Rust yang kuat, rasa percaya diri saat menyentuh codebase jadi jauh lebih besar. Dengan kepercayaan diri ini, refactor di bagian inti pun tidak terasa menakutkan, dan pada akhirnya produktivitas serta maintainability meningkat besar. Tapi justru untuk efek seperti inilah testing dipakai. Kalau tidak ada testing, compiler yang ketat memang sangat membantu, tetapi kalau test ditulis dengan baik, bahasa apa pun memungkinkan refactor dengan percaya diri
Lebih baik kalau hal yang memungkinkan dibuktikan secara statis oleh compiler. Testing paling optimal dipakai hanya untuk situasi yang sulit dijamin secara statis. Bentuk ideal paling puncak tentu formal verification, tetapi itu sangat sulit secara praktis, jadi ini bukan kaidah umum, meskipun secara prinsip benar
Testing yang baik dan sistem tipe yang dimanfaatkan dengan baik sama-sama efektif untuk menangkap bug. Tetapi menulis test kadang mengingatkan saya pada komik xkcd “Standards”. Mirip seperti memperbaiki standar dengan membuat standar baru, kita juga memperbaiki bug dengan menulis lebih banyak kode. Meski begitu, pemeliharaan sistem tipe ditangani perancang bahasa, jadi tidak perlu dikelola per proyek
Setiap kali refactor kode, test juga harus ikut direfactor, jadi pekerjaannya jadi dua kali lipat
Menurut saya, sistem tipe Rust atau F# paling bersinar saat melakukan refactor kode. Istilah refactoring tanpa rasa takut memang sangat pas
Contoh Zig itu mengejutkan. Kelihatannya terlalu tidak stabil sampai saya tidak paham bagaimana desain seperti itu bisa dianggap bagus
Saya rasa ini kemungkinan bug. Tetapi untuk bahasa yang sangat berpusat pada kreatornya seperti Zig, agar bug bisa diperbaiki, penting juga bahwa sang kreator mengakui itu sebagai bug. Kalau dianggap memang disengaja, desain seperti itu bisa saja terus dipertahankan
Semua bahasa punya sedikit desain yang rawan. Misalnya, di Go atau Zig kita selalu harus memanggil
mutex.unlock()secara eksplisit, dan lock tidak otomatis dilepas saat keluar dari scope. Sebaliknya, konversi antartipe angka dengan operatorasdi Rust juga terlalu mudah, dan gara-gara itu saya pernah menghabiskan seharian mencari bugAwalnya saya tidak melihat error itu, lalu sadar setelah membaca komentar ini
Saya membayangkan linter bisa saja memberi peringatan dengan menangkap referensi error yang tidak ada dalam sistem, dan menyarankan penggunaan
switchSaya tadinya mengira error set dibuat berdasarkan function signature. Cukup unik juga
Saya suka bahwa static type system yang kuat dan sound bisa memberi begitu banyak kemampuan. Saya juga pernah mengalami betapa mudahnya melakukan refactor besar-besaran di codebase Haskell (1 juta SLOC). Bahkan tanpa fitur yang sangat canggih pun, hanya dengan sistem tipe saja itu sudah memungkinkan
Rust memang mendeteksi dengan benar ketika lock masih dipegang di batas
await, tetapi apakah melepas lock sebelumawaititu benar-benar aman tetap membutuhkan konteks tambahan. Menurut saya lock harus tetap dipegang sampai transaction commit dibuat; kalau dilepas sebelumawait, bisa timbul masalah konkurensi. Saya tidak terlalu paham Rust async, tetapi setelah commit bukankah seharusnya diblokir denganjoinatauselectKalau memang perlu mempertahankan lock saat
await, gunakan async-aware mutex. Cratefuturesatautokiomengimplementasikan lock seperti itu. Biasanya dipakai ketika lock dipegang lama atau perlu dipertahankan melewatiawait. Biayanya lebih tinggi dibanding lock biasaJika lock memang harus dipertahankan bahkan melewati batas
await, Anda bisa memakai async-aware mutex dari Tokio. Lihat dokumentasi tokio/sync/struct.Mutex