Rust'ın Beklenmedik Üretkenlik Artışı
(lubeno.dev)- Rust, güçlü güvenlik garantileri sayesinde büyük kod tabanlarında bile refactoring işlemlerini güvenle yapmayı mümkün kılar; bu da üretkenliği ve bakım yapılabilirliği artırır
- Asenkron zamanlama ile ilgili hataları derleyici önceden tespit eder, tanımsız davranışı önleyerek kararlılığı güçlendirir
- TypeScript gibi dillerde gevşek tip sistemi nedeniyle asenkron hatalar çoğu zaman production ortamında fark edilir
- Rust'ın tip sistemi, kod değişikliklerinin etkisini açıkça göstererek karmaşık projelerde güveni ve deneme isteğini artırır
- Zig, Rust'ın aksine hata işlemede daha gevşek denetimler nedeniyle yazım hatalarından kaynaklanan bug'ları kaçırabilir; bu da güvenilirliği düşürür
Özet ve arka plan
- Lubeno'nun backend'i %100 Rust ile yazıldı ve kod tabanı büyüdükçe tamamını zihinde tutmanın zorlaştığı bir aşamaya ulaştı
- Büyük projelerde genelde değişikliklerin yan etkilerini doğrulamak zorlaştığından üretkenlik düşüşü yaşanır
- Rust'ın güvenlik garantileri, kod değiştirildiğinde etkinin nereye uzandığını açıkça göstererek refactoring konusundaki korkuyu azaltır
- Bu da uzun vadeli bakım yapılabilirliğe ve üretkenlik artışına katkı sağlar
- Bu yazı, Rust derleyicisinin bir asenkron bug'ı tespit ettiği bir örnekle başlayıp Rust'ın üretkenlik avantajlarını inceliyor
Rust'ın güvenlik garantilerine bir örnek
- Sorun durumu: Bir struct, eşzamanlı erişim için mutex ile sarılıyor; kilit alındıktan sonra asenkron işlem yapılıyor
let lock = mutex.lock(); db.insert_commit(commit).await; - Sorunun fark edilmesi: rust-analyzer hata göstermese de router tanım dosyasında derleme hatası oluştu
.route("/api/git/post-receive", post(git::post_receive)) ^^^^^^^^^^^^^^^^^ error: future cannot be sent between threads safely - Neden analizi:
- Web framework'ü, her HTTP bağlantısı için asenkron task oluşturur ve görev zamanlayıcısı bu task'ları thread'ler arasında taşıyabilir
- Mutex, kilidin aynı thread'de bırakılmasını gerektirir;
.awaitnoktasında thread değişirse tanımsız davranış oluşabilir - Rust derleyicisi kilidin ömrünü izler ve başka bir thread'de bırakılma ihtimalini tespit eder
- Çözüm yöntemi:
.awaitöncesinde kilidi bırakmak - Önemi: Rust, geliştirme ortamında yeniden üretmesi zor asenkron bug'ları derleme zamanında engeller
TypeScript ile karşılaştırmalı örnek
- Sorun durumu: TypeScript kodunda asenkron yönlendirme bug'ı oluşuyor
if (redirect) { window.location.href = redirect; } let content = await response.json(); if (content.onboardingDone) { window.location.href = "/dashboard"; } else { window.location.href = "/onboarding"; } - Sorunun nedeni:
window.location.hrefanında yönlendirme yapmaz; yönlendirmeyi zamanlar ve kod çalışmaya devam eder- race condition nedeniyle istenmeyen yönlendirme oluşur
- Çözüm yöntemi:
ifbloğunareturneklemekif (redirect) { window.location.href = redirect; return; } - Sınır: TypeScript'te ömür takibi veya borrowing kuralları olmadığından bu tür hatalar derleme zamanında tespit edilemez
- Production ortamında fark edilir ve debug için uzun zaman harcanır
Rust'ın refactoring avantajları
- Web geliştirmede Python, Ruby, JavaScript/Node.js başlangıçta yüksek üretkenlik sunar; ancak kod tabanı büyüdükçe gevşek bağlılık nedeniyle değişiklik yapmak zorlaşır
- Değişiklikten sonra beklenmedik hatalar ortaya çıkar ve kodu değiştirme isteği azalır
- Rust'ta tip sistemi, değişikliklerin etkisini açıkça gösterdiği için refactoring korkusunu azaltır
- Örneğin, “bu değişiklik başka bölümleri etkileyebilir” uyarısı sorunları önceden engeller
- Kod tabanı büyüse bile üretkenlik artabilir; mevcut kod yeniden kullanılabilir ve değişiklik yapılırken kararlılık korunur
Testlerle karşılaştırma
- Testler, refactoring sırasında regresyonu önlemede faydalıdır; ancak derleyici tarafından zorunlu kılınmadıkları için atlanabilirler
- Test yazarken soyutlama düzeyi, davranış mı yoksa uygulama ayrıntıları mı test edileceği ve hataları gerçekten önleyip önlemediği gibi kararlar vermek gerektiğinden zihinsel yük yüksektir
- Rust'ta derleyici, yaygın hataları önceden engeller ve testlerle ilgili karar yükünü azaltır
- Tip sistemiyle doğrulanamayan özellikler ise testlerle tamamlanır
Zig ile karşılaştırma
- Zig, Rust'a benzer bir sistem programlama dili olsa da hata işlemede daha gevşektir
- Örnek hata işleme kodu:
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", .{}); } }; } AccessDenidyazım hatası bug'a yol açar; ancak Zig derleyicisi bunu sayı olarak değerlendirip derlemeyi başarılı sayar
- Örnek hata işleme kodu:
- switch ifadesi kullanıldığında yazım hatası tespit edilirken, if ifadesinde göz ardı edilir; bu da güvenilirlik sorununa yol açar
- Rust, bu tür tasarımsal boşlukları önler; yazım hataları ve mantıksal hatalar sıkı biçimde denetlenir
Çıkarımlar
- Rust, güvenlik garantileri ve katı tip sistemiyle büyük projelerde üretkenliği ve kararlılığı artırır
- Asenkron bug'lar gibi karmaşık sorunları bile derleme zamanında tespit ederek bakım maliyetini düşürür
- TypeScript ve Zig örnekleri, gevşek denetimlerin doğurduğu riskleri gösterirken Rust'ın katı derleyicisinin değerini vurgular
- Rust, web geliştirmede de yalnızca başlangıç üretkenliği açısından değil, uzun vadeli kod tabanı yönetimi için de güçlü bir araç haline gelir
3 yorum
Bunu görünce her seferinde, "bu en iyisi, bu çok güçlü bir dil!!" denildiğinde aklıma şu geliyor:
Düşündüğümden çok daha az Rust geliştiricisi var, o yüzden herkesi Rust kullanmaya mı ikna etmeye çalışıyorlar acaba??
Rust ile ilgili öneri yazıları bana sanki gurme programlarındaki “Deneyin! Deneyin!” gibi geliyor; herhalde bunu sadece ben düşünmüyorumdur, değil mi?
Hacker News görüşleri
Geçen yıl Rust ile yazılmış
virtio-hostağ sürücüsünü port ettim. Backend’i, kesme mekanizması geçişini ve kütüphaneden bağımsız çalışan bir sürece dönüştürmeyi yaptım. Bellek eşleme, VM kesmeleri, ağ soketleri ve çoklu iş parçacığını kapsayan karmaşık bir programdı. Rust deneyimim neredeyse yoktu,virtiodeneyimim de azdı ama proje derlenir hale geldiğinde neredeyse kusursuz çalıştı.Dropile ilgili tek bir hata dışında onu da kolayca düzelttim. Rust kütüphanelerinin yanlış kullanılmayacak şekilde tasarlanmış olmasının bana çok yardımcı olduğunu düşünüyorumBence Rust harika. Ama
hrefatama hatasının TypeScript’in suçu olduğu görüşüne katılmıyorum. Sorunun özü,hrefayarlansa bile sayfa geçişinin hemen olmaması ve daha sonra işlenmesi. Aynı sorun Rust’ta da olabilir. Eğer Rust’ta birset_hreffonksiyonu olsaydı ve bu davranış daha sonra işlenseydi, aşağıdaki gibi bir kod mümkün olurdu:set_href('/foo')
if (some_condition) { set_href('/bar') }
Rust’ta bunun böyle tasarlanacağını sanmıyorum. Setter içinde davranış gerçekleşmesi iyi bir kütüphane tasarımı değil ve
hrefatandığı anda sayfa geçişinin olmaması da tuhaf. Rust standart kütüphanesinde böyle aptalca bir uygulama olmazdı. Bu Rust vs TypeScript meselesi değil, Rust standart kütüphanesi ile Web Platform API’si arasındaki fark. Rust’ın böyle bir kullanıcı deneyimi sunmayacağına katılıyorumResmî olarak konuşursak, setter içinde anında davranış olacak şekilde tasarlamak arzu edilir değil. İsimlendirmeyi de
navigate_to(href)gibi yapmak daha doğru olur. Tarayıcı ortamında JS kodunun tamamı callback olarak çalıştığı ve event loop tarafından kontrol edildiği için, anında çalışmaması da doğal bir durumRust örneği ilginç ama yalnızca TypeScript örneğinden TS’nin büyük ölçekli projelere uygun olup olmadığını anlayamayız. Ruby’de sık sık çalışma zamanında hata yakalamam gerektiği için tedirgin oluyorum ama sonuçta commit atmadan önce iyi çalışıyor ve kodu okuyup değiştirmek kolay olduğu için memnunum. Konum değiştirme meselesi JavaScript’in problemi ve TS’nin miras aldığı bir şey. JS özelliklerin keyfi biçimde değiştirilmesine izin verdiği için böyle olmuş. Ama sayfa anında kaybolmadığı için, bu davranış öğrenildikten sonra mantıklı geliyor
Teknik olarak Rust’ta
set_hreffonksiyonunun()veya!döndürmesiyle anlamı daha net ima edilebilir. Ama koşullu yönlendirmelerde yanlış kullanımı yakalamak yine de zor olurduBenim amacım, Rust’ın sahiplik modeliyle
window.set_href('/foo')çağrısındawindowsahipliğinin alınacağı ve bu yüzden API’nin iki kez çağrılmasını imkânsız kılacak şekilde tasarlanabileceğini söylemekti. TypeScript’te ömür takibi diye bir kavram olmadığı için bu mümkün değil. JS API’si zaten mevcut olduğundan TypeScript tarafında sahiplik sistemi eklemenin de bir yolu yok. Rust’ın çeşitli özelliklerinin birleşerek daha güçlü güvenceler sağlayabildiğini göstermek istemiştimRust’ın daha iyi olduğunu savunurken dayanağın en sonunda “Rust programcıları daha iyi olduğu için” noktasına çıktığı izlenimi veriyorsun. Rust programcılarının böyle döngüsel bir argüman kuracağını sanmıyorum
Atamadan sonraki kod, açıkça erken
returnedilmediği sürece çalışmaya devam eder. Cidden, bir değer atamanın script çalışmasını durduracağını neden düşündüğünü anlamıyorum. TS örneğinde bağlam eksik olabilir ama bunu “data race” diye sunmak tuhaf bir örnekwindow.location.hrefdeğerine atama yapıldığında tarayıcıda o bağlantıya gitme yan etkisi oluşur. Bu davranış beklenmedik ve basit bir atamanın yeni sayfa yüklemesiexecvegibi bir his verdiğinden, JS çalışmasının anında duracağını düşünmek de çok garip değil. Programlama yaparken buna güvenmemek gerekir ama davranış gerçekten tuhaf olduğu için kafa karıştırıcı olabilirİnsan bunu düşünmüş olsun ya da olmasın, böyle bir hata biri söylediğinde düzeltmesi nettir. Yazarın asıl anlatmak istediği, TS’nin yakalayamadığı bu tür hataların gerçekte bulunmasının zor olabildiği ve çok zaman alabileceğidir
exit(),execve()vb. gerçekten çalışmayı anında durdurduğu için, yönlendirme davranışının da böyle olduğunu düşünmek mümkünSırf kendi deneyimini paylaştı diye bunu sorun etmek garip
Bu atama, sayfadan ayrılmaya yol açan büyük bir yan etki taşıyor. Bunu anında çalışan asenkron bir aksiyon gibi düşünmek de çok mantıksız değil. Ben de böyle varsaymıştım
Bu, geliştiricinin statik tip sistemlerinin faydalı olduğunu fark ettiğini anlatan bir hikâye. Böyle yazıları gördüğümde hep eğleniyorum
Avantajların çoğu aslında statik tipli, yani derlenen bir dil kullanmaktan gelmiyor mu? Java, Go, C++ için de benzer şeyler geçerli. TypeScript’in bir numarası var; JS’ye derleniyor ve JS’nin sorunlarını da miras alıyor ama yine de kullanılabilir. Rust’ın tip sistemi daha katı olduğu için fazladan derleme zamanı kontrolleri alabiliyorsunuz ama buna karşılık öğrenmesi ve okuması daha zor diye düşünüyorum
Bir ölçüde katılıyorum ama Rust’ın tip sisteminde sahiplik, paylaşımlı/özel erişim, thread güvenliği, sum type gibi daha fazla boyut var. Sahiplik/ödünç alma sistemi sayesinde argüman aktarımının geçici bir görünüm mü yoksa tamamen devretme mi olduğu netleşiyor. Bu, büyük programlarda veya harici kütüphaneler kullanırken çok faydalı. Örneğin Go’nun slice tipi, hangi işlemlerin çalışma zamanında izinli olduğunu net göstermiyor ve onu salt okunur şekilde ödünç vermenin yolu da belirsiz. Rust tip sistemi düzeyinde thread güvenliği garantisi verebildiği için, başka dillerde çalışma zamanında bulunması zor olan data race’leri derleme zamanında engelleyebiliyor
Bütün statik tipli dilleri tek bir şeymiş gibi görmek, union(sum) type ve pattern matching’in gerçek gücünü henüz hissetmemekten kaynaklanıyor. Bir kez union type’a alışınca diğer geleneksel statik tipli diller tatmin etmemeye başlıyor
Büyük avantajlardan biri de
traits/impl traits. Rust, C#’taki Extension Method’a benzer şekilde herhangi bir tipe sonradan trait eklenmesine izin veriyor. Çoğu dilde bir tip kütüphanede tanımlandığında neyse odur, ama Rust’ta basit tiplere işlevselliği aşama aşama eklemeye devam edebilirsiniz. Bu late-bound karakter, tip sistemine bir tür dinamizm katıyor. Biraz abartılı söylemek gerekirse Rust’ın gerçek süper gücü borrow checker’dan çok tip sisteminin açıklığı ve esnekliği. Her şeyi en baştan tasarlamak zorunda değilsiniz; kademeli olarak genişletebilirsinizHer statik tipli dil aynı etkiyi üretmez. Java sonuçta
Objectve çalışma zamanı cast’lerine dayanır. Go’da enum yok. C++’avarianteklendi ama güvenli kullanmak içintry/exceptbenzeri manuel işlemler gerekiyor, bu da yapısal olarak rahatsız ediciRust’ı öğrenmenin zor olduğundan bahsediliyor ama aslında gerçekten öğrenirseniz zor değil. Bir şeyleri biraz gelişi güzel yazıp çalışır hale getirmek, kodlamanın erken aşamalarında önemli olabiliyor ve Rust bu yaklaşıma pek dost bir dil değil. Başlangıç dili olarak önermem ama okunması zor değil
Rust’ın güçlü güvenlik özellikleri sayesinde kod tabanına dokunurken özgüven artıyor. Bu özgüvenle çekirdek bölümleri refactor etmek de korkutucu olmuyor ve sonuçta üretkenlik ile bakım yapılabilirlik büyük ölçüde artıyor. Ama zaten bu etki için test yazıyoruz. Test yoksa katı bir derleyici çok yardımcı olur ama testleri iyi yazarsanız hangi dil olursa olsun güvenle refactor edebilirsiniz
Mümkün olan kısımları derleyicinin statik olarak kanıtlaması daha iyidir. Testleri, yalnızca statik güvencenin zor olduğu durumlarda kullanmak en uygunu. İdeal son nokta formel doğrulama olurdu ama pratikte çok zor; genel bir çözüm değil ama ilke olarak doğru
İyi testler ve iyi kullanılan tip sistemi, ikisi de hata yakalamada etkili. Ama test yazmak bazen xkcd’deki “Standards” karikatürünü hatırlatıyor. Standardı düzeltmek için bir standart daha üretmek gibi, burada da hataları yakalamak için daha fazla kod yazıyoruz. Yine de tip sistemi bakımını dil tasarımcıları yapıyor; her projede ayrıca yönetmek gerekmiyor
Kodu her refactor ettiğinizde testleri de refactor etmek gerektiği için iş iki katına çıkıyor
Bence Rust ya da F#’ın tip sistemi, kodu refactor ederken en çok parlıyor. “Korkusuz refactor” ifadesi tam oturuyor
Zig örneği sarsıcı. O kadar güvensiz görünüyor ki böyle bir tasarımın nasıl iyi bulunabildiğini anlamıyorum
Bunun muhtemelen bir bug olduğunu düşünüyorum. Ama Zig gibi yaratıcısı merkezli bir dilde, bug’ın düzeltilmesi için o yaratıcının da bunun bug olduğunu kabul etmesi önemli. Eğer niyetli bir tasarım olarak görülürse aynı şekilde devam edebilir
Her dilde biraz güvensiz tasarım vardır. Örneğin Go veya Zig’de
mutex.unlock()her zaman açıkça çağrılmalı; scope dışına çıkınca otomatik serbest bırakma yok. Öte yandan Rust’takiasoperatörü gibi sayısal tipler arası dönüşümler fazla kolay ve ben bunun yüzünden bütün gün bug aradımBaşta o hatayı fark etmemiştim, bu yorum sayesinde gördüm
Linter, sistem içinde var olmayan hata referanslarını yakalayıp
switchkullanımını öneren uyarılar verebilir diye düşünüyorumHata kümesinin fonksiyon imzasına göre üretildiğini sanıyordum. Biraz tuhafmış
Güçlü ve sağlam bir statik tip sisteminin çeşitli özellikler sunmasını seviyorum. Ben de Haskell kod tabanında (1 milyon SLOC) büyük ölçekli refactor’ların kolay olduğu deneyimini yaşadım. Gelişmiş özellikler olmadan bile, yalnızca tip sistemiyle bu mümkün olabiliyordu
Rust,
awaitsınırında kilidi tutuyor olmayı doğru biçimde algılamış ama o kilidiawaitöncesinde bırakmanın gerçekten güvenli olup olmadığı ek bağlam gerektiriyor. Bana göre kilit, transaction commit oluşturulana kadar tutulmalı;awaitöncesi bırakılırsa eşzamanlılık sorunları doğabilir. Rust async konusunda uzman değilim ama commit sonrasındajoinya daselectile engellemek gerekmiyor mu diye düşünüyorumEğer
awaitsırasında kilidi elde tutmanız gerekiyorsa, async-aware bir mutex kullanabilirsiniz.futuresveyatokiocrate’leri bu tür kilitler sunuyor. Genelde uzun süre tutulacaksa ya daawaitnoktaları arasında kilit korunacaksa kullanılır. Normal kilitlerden daha maliyetlidirawaitsınırlarında da kilidi korumanız gerekiyorsa Tokio’nun async-aware mutex’ini kullanabilirsiniz. tokio/sync/struct.Mutex belgelerine bakın