Stackoverflow'un 2020 yılı geliştirici anketine göre en sevilen programlama dilinin Rust olduğunu söyleyebiliriz. Hatta bu son yıllarda hep böyle. Nedir onu bu kadar özel yapan merak ediyorum. Bunu anlamanın tek yolu onunla bir şeyler karalamak. Resmi dokümantasyonuna göre ilk tespitlerim şunlar.
- Rust geliştiricilerin Rustaceans deniyor
- İlk söylenmesi gereken şey Rust'ın amacının sistem seviyesinde programlama yapmak olduğu. C ve C++ gibi bir dil olduğunu düşünebiliriz.
- Şu sıralar çok popüler olmaya başlayan Deno'nun Rust ile yazıldığını söylesem...Ya da Microsoft Azure IoT Edge'in çok büyük bir kısmının onunla yazıldığını ifade etsem. İşletim sistemleri (TockOS, Tifflin, RustOS, QuiltOS, Redox), oyun motorları, derleyiciler, container'lar, VM'ler, Linux dosya sistemleri vs, ls komutunun alternatifi olan exa vs... Yani Rust ile yazılım ve yazılım platformları geliştirildiğini ifade edebiliriz. Bu nedenle Rust donanım odaklı bir dil desek yeridir. Donanımı etkin kullanmaya çalışır.
- Rust ortamında Garbage Collector gibi bir mekanizma yoktur. Amaç çalışma zamanı performansının artırılmasıdır. Dilin hedeflerinden birisi de hızdır zaten. (Hatta değişkenler bile varsayılan olarak immutable oluşur. Ufak veri yapılarında-Data Structures bu önemli bir performans kazanımıdır. Yüklü veri yapılarında ise mutable kullanımı daha uygun olabilir nitekim referans etmek yığının bir kopyasını oluşturarak çalıştırmaktan daha mantıklıdır)
- Diğer iki önemli hedefi de eş zamanlılık (Concurrency) ve güvenli bellek kullanımıdır.
- Rust derlemeli bir dildir. Hatta derleme çıktısı WebAssembly'da olabilir.
- Pek çok diğer modern dilde olduğu gibi Rust'ın da etkili bir paket yönetim mekanizması vardır. İsmi de gayet makul ve mantıklı. Cargo
- Dilin arkasında Mozilla Labs'ın gücü var. Hatta servo isimli yüksek performans vaat eden tarayıcı motoru da Rust ile geliştirilmiş.
- Dilin diğer karekteristik özelliklerini elbette kod üstünde anlamaya çalışacağım.
Önce Rust ortamını hazırlamak lazım. Ben Heimdall (Ubuntu 20.04) üstünde ilerliyorum. Geliştirmeler için Visual Studio Code'dan yararlanacağım.
curl https://sh.rustup.rs -sSf | sh
# Dilin genel özelliklerini tanımak için bir dosya üstünde çalışalım
touch WhoAreYouRust.rs
Dilin genel özellikleri ile ilgili dikkatimi çeken birkaç anahtar noktayı kendime not olarak alayım. (İlk boğum örnek uygulamanın adını işaret ediyor)
Örnekler arttıkça takip etmek zorlaşabilir. Bu nedenle konuları aşağıdaki sırayla incelemenizde yarar var.
- factorial;
- mutable değişken tanımlama,
- recursive metot parametresi için match kullanımı,
- kütüphane bildiriminin nasıl yapıldığı
- ekran girdisinin parse edilmesi
- lucky_number;
- harici kütüphane nasıl bildirilir (toml),
- for döngüsünde aralık bildirimi,
- parse sonucunun match ile ele alınması,
- continue, break kullanımı,
- compare işlem sonucunun match ile ele alınması,
- fundamentals;
- immutable olmak ya da olmamak,
- constant'lar ciddi ciddi immutable
- shadowing,
- rust statik türlü bir dildir,
- Destructuring,
- tuple kullanımı,
- fonksiyonlarda return kullanma zorunluluğu olmaması,
- fonksiyonlarda match kullanımı,
- for döngülerinde iter ve rev ile ileri geri hareket edebilme,
- loop döngüsü,
- slice veri türü,
- ownership;
- String tipleri arasında yapılan atama sonrası atanan tipin scope dışında kalması (move) ,
- Metot parametre ve dönüşlerinde sahipliğin (ownership) değişmesi,
- & referencing ve * deReferencing operatörleridir,
- borrowing,
- aynı scope içinde birden fazla mutable referans atamasına izin yok, (Data Race kısıtı) ,
- structs;
- struct update syntax,
- tuple görünümlü struct kullanımı,
- impl blokları ile struct veri yapısına kendi çalışma zamanı örneği ile çalışacak metotlar eklenebilir
- enums;
- enum değişkenleri farklı türde ve sayıda veri yapısını parametre olarak kullanabiliyor,
- Null tipi yok ama Option üstünden None tanımlanabilir,
- enum veri yapısı ile pattern matching kullanılabilir,
- Option ile match kullanımı,
- mercury; (bir module örneği)
- modül öğeleri varsayılan olarak private nitelik taşır (yani modül dışında kullanılamaz),
- modül içi struct veri yapıları da varsayılan olarak private özelliklidir ve hatta alanları da,
- crate (sandık) içerisindeki bir enstürmana (örneğin bir struct) nasıl erişiriz (:: notasyonu),
- pub ile modül üyesini public kullanıma açarız,
- super ile bir üst katmandaki elemanlara ulaşabiliriz (bulunduğum modülden bir üst modüldeki bir elemana ulaşmak gibi),
- use ile modüle elemanlarına daha kolay ulaşabiliriz,
- modüller dosya/klasör hiyerarşisine göre kullanılabilirler (azon örneğine bakın)
- collections;
- 3 temel koleksiyon var; vector (Depicable Me'deki gru'nun rakibi olan değil) , string ve hash map,
- vector türünde dikkat çeken fonksiyon ve operatörler pop, push, iter, iter_mut, * (dereference), & ,
- vector ile enum veri yapısı da kullanılabilir,
- String, UTF-8 kodlamasını kullanır,
- String'lere push_str veya push ile ekleme yapılabilir,
- Çok fazla String'in birleştirilmesi gerektiğinde + yerine format! makrosu tercih edilmelidir,
- Bir String'in uzunluğu hiç de beklediğimiz sayı olmayabilir,
- Key:Value çiftlerinden oluşan koleksiyonlar için HashMap,
- Bir HashMap'in key ve value dizileri vector koleksiyonlarından oluşturulabilir,
- error_handling;
- iki tür hata kategorisi var: Kurtarılabilir olanlar(recoverable) ve tabii ki kurtarılabilir olmayanlar(unrecoverable)
- panic! makrosu kurtarılabilir olmayan senaryolarda söz konusu,
- Result<T,E> tipi,
- Winding ve Unwinding,
- RUST_BACKTRACE=1 cargo run ile stacktrace benzeri hata detayına ulaşılabilir,
- Error propagating, unwrap, expect,
- ? operatörü ile Result<T,E> çıktılarını ele almak,
- generics;
- kod tekrarının önüne geçmekte sıklıkla kullanılır,
- struct ve sturct'a uygulanan metotlar generic olabilirler,
- generic tip için Trait bildirimi yapmak gerekebilir,
- trait ile struct'lar için ortak davranış sözleşmeleri bildirebiliriz (tam olarak interface değil, tam olarak abstract sınıf da değil. Değişik bir şey),
- trait'ler boyutsuzdu bu nedenle bir vector dizisi yapılmak istendiğinde box yapısına başvurmak gerekir,
- trait'ler fonksiyonlara parametre olarak geçirilebilir ve hatta döndürülebilir,
- built-in trait'leri yeniden programlayabiliriz (operator overloading),
- lifetimes;
- Tüm referans türlerinin bir yaşam ömrü (lifetime) vardır,
- lifetime ve scope kavramları birbirlerine benzer ama aynı şey değildirler,
- generic lifetime parametreleri (' ile tanımlananlar),
- Bir fonksiyon lifetime referans ile dönüyorsa parametrelerinden en az birisinin de lifetime referans olması gerekir,
- struct yapılarında referans türlü alanlar varsa lifetime annotation kullanmak gerekir,
- testing;
- bir kütüphane oluşturduğumuzda içerisine otomatik olarak test modülü eklenir
- test modülü #[cfg(test)] niteliği (attribute) ile test fonksiyonları da #[test] niteliği ile işaretlenir
- assert!, assert_eq!(left, right) ve assert_ne!(left, right) makroları ile bağımsız kabul, eşitlik ve eşitsizlik kriterleri test edilebilir
- assert! makrosunda teste ait detay bilgiler verilebilir,
- panik fırlatmasını istediğimiz senaryolarda should_panic niteliği kullanılır,
- ignore niteliği ile bir test vakasını atlatabiliriz
- fonksiyonlarda kendi yardım dokümanlarımızı da tanımlayabiliriz. /// kısımları içerisinde markdown kurallarına göre detaylı yardım sunabiliriz,
- reader (csv tarzı dosya okuyan bir örnek)
- closures/closures2,
- rust dili de fonksiyonel dil özelliklerini (fonksiyonları değişkene atama,parametre olarak geçme,fonksiyondan fonksiyon döndürme vb) etkin bir şekilde kullanır,
- closure, iterator, pattern matching ve enum kavramları Rust'ın öne çıkan fonksiyonel dil yetenekleridir,
- closure'ları parametre olarak geçtiğimiz generic kurgularda FnOnce, FnMut ve Fn trait'lerinden en az birisi kulanılmalıdır,
- bir closure bulunduğu ortamdaki değişkenleri sahiplenerek kullanabilir (sahiplendiğini değiştirmesi gerekirse FnMut Trait olmalıdır)
- iterators/iterators2/own_iterator,
- itereator kalıbı bir nesne dizisinde ileri yönlü hareket ederken her bir dizi öğesi için bir fonksiyonelliği çalıştırmak gibi işlemlerde kullanılır,
- iterator'lar standart kütüphanedeki Iterator isimli Trait'i uygularlar,
- iter() arkasından gelen map, filter, for_each, find vb pek çok fonksiyon (ki bunlara iterator adaptor deniliyor) parametre olarak closure alır,
- filter, geriye bool döndüren bir adaptor'dür ve kendi nesne yapılarımızın filtrelenmesi gibi ihtiyaçlarda epeyce işe yaramaktadır
- Kendi veri yapılarımız (genellikle struct'lar) veya diğer koleksiyon türleri için (örneğin hash map) kendi iterator nesnelerimizi yazabiliriz,
- own_iterator örneğindeki lifetime kullanımının sebebi nedir?
- hof,
- HOF : Oflamak puflamak sıkılmak anlamında değildir. Higher Order Function demektir.
- smart pointers,
- Verinin adresi dışında metadata bilgisi de taşıyan ve genellikle veriyi sahiplenen (ownership) veri yapılarıdır,
- Reference Counting Smart Pointer veri yapısı ile bir veriyi sahiplenen n adet referans kullanmak mümkündür,
- Kendi Smart Pointer'larımızı da yazabiliriz.
-
- (Dereference) operatörünün kullanılabilmesi için Deref Trait'inin kendi smart pointer türümüz için de yazılması gerekir
- reference_counting,
- Reference Counting, bir değerin birden fazla sahibi olması istenen durumlar için geçerli bir konudur
- Bu kabiliyet için Rc yapısı kullanılır,
- Rc aynı değeri işaret eden referansların muhasebecesi gibidir. Değeri işaret eden referans kalıp kalmadığını hesaplar, kalmamışsa değer temizlenir.
- Rc clone fonksiyonu Deep Copy yapmaz.
- Clone'lama olduğunda sayaç bir artar, scope dışına çıkıldığında ise bir azalır. Taa ki hiçbir referans kalmayana kadar.
- fearless_concurrency/join_handle/multi_join/counter/message_passing/mutexes,
- Birbirinden bağımsız çalışan program parçaları için Concurrent, aynı anda çalışan program parçaları içinse Parallel terimlerini kullanıyoruz,
- Rust'ın ownership, borrowing, type system gibi güvenli bellek ve verimlilik odaklı kavramları eş zamanlı (Concurrent) programlamada da etkisini gösteriyor. Çünkü diğer dillerde çalışma zamanında ortaya çıkabilecek Concurrency hataları Rust dilinde henüz derleme aşamasında ortaya çıkıyor. (Bu yüzden Fearless Concurrency diye bir kavram oluşmuş)
- Main thread önceden başlatılan başka thread'ler de olsa en öncelikli sırada çalışır,
- JoinHandle ile bir thread'in işlerini bitirene kadar onun parent thread'inin beklemesini sağlayabiliriz,
- thread'ler arası veri taşınması istenen durumlarda move closure'ından yararlanılır,
- thread'den dönen değeri pattern matching ile JoinHandle nesnesi üzerinden yakalayabiliriz,
- Go'dan gelen bir felsefe => Hafızayı paylaşarak iletişim kurmayın; bunun yerine iletişim kurarak hafızayı paylaşın
- thread'ler arası güvenli haberleşme için channel kullanılır,
- mpsc => multiple producer, single consumer (Çoklu üretici, tek tüketici)
- n sayıda transmitter kullanırken klonlamak gerekir,
- Kanallar (channels) tekil mülkiyet (single ownership) Mutex ise çoklu mülkiyet (multiple ownership) özelinde düşünülebilir,
- Mutex ile eş zamanlı kullanılmak istenen veriye t anında sadece tek bir thread'in erişmesi garanti edilir (Birden fazla thread söz konusu olduğunda Mutex'i Arc - Atmoic Reference Counting ile birlikte klonlayarak kullanırız)
- patternsmatching,
- let ile yapılan değişken atamalarında da aslında eşitliğin sağ tarafında ne olursa olsun eşitliğin solundaki pattern (şablon) ile eşleştirilir (matching) Yani let pattern=matching; söz dizimi kuralı geçerlidir.
- if let, while let ile şablon eşleştirme uygulanabilir,
- enumarate ile for döngüsünde hareket edilirken de bir pattern matching (şablon eşleştirme) söz konusudur,
- match ifadelerinde | ile n adet şablon veyalanarak (or) kullanılabilir,
- match ifadelerinde sayısal veya karakter bazlı aralıklarda eşleştirme için kullanılabilir (..=)
# Rust kodlarını derlemek için
rustc WhoAreYouRust.rs
# Çalıştırmak içinse
./WhoAreYouRust
# Cargo'dan bahsetmiştik (Kargo grubu geldi aklıma. Ne dinlerdim ama?)
# Cargo ile derleme, paket yönetimi ve daha bir çok işlem yapılabiliyor.
# Örnekleri Cargo ile geliştireceksek
# Klasör yapısını inceleyin ve toml dosyasına bakın. Projenin genel özellikleri ile bağımlı olduğu diğer paketler burada yer alacak.
# Kodlar src altındaki main.rs'tedir.
cargo new factorial
cd factorial
# Cargo üstünde build için
cargo build
# ve çalıştırmak için
cargo run
# Derleme yapmadan kodu kontrol etmek için
cargo check
# Release almak için
cargo build --release
# factorial örneğinde rand isimli rastgele sayı üretme kütüphanesinin kullanımı için toml dosyasında değişiklik yapıldı. (Bul bakalım)
# rand kütüphanesinin 0.5.3 sürümünü kullandık. Ek kütüphaneler cargo build komutu ile indirilir. Güncellenmeleri gerektiğinde cargo update komutu kullanılabilir.
# Elbette benzer işleri yapan fonksiyonellikleri bir arada tutmak vb işlerde module, package ve crate gibi yapılar kullanılıyor
# Yeni bir modül oluşturmak için aşağıdaki gibi bir komutu kullanabiliriz
cargo new --lib mercury
# Dosyalara bölünmüş modül kullanımı örneği için aşağıdaki yolu izleyebiliriz
# azon altındaki client.rs main fonksiyonunu içerir ve libraries içerisindeki modülü kullanmaktadır
# client.rs'in libraries klasöründeki modülü kullanabilmesi için modül adının mod.rs olması gerekiyor
mkdir azon
cd azon
touch client.rs
mkdir libraries
touch libraries/mod.rs
# derleme için aşağıdaki komutu kullanmak yeterli
rustc client.rs && ./libraries/
# sonrasında şu komutla uygulamayı çalıştırabiliriz
./client
# Bir kütüphane oluştuğunda içerisinde test modülü otomatik olarak oluşur
# Testleri çalıştırmak içinse aşağıdaki terminal komutunu kullanmak yeterlidir
cargo test
# Test vakalarından örneğin belli bir tanesini çalıştırmak istersek
# testin adını parametre olarak vermemiz yeterli olur
# Hatta bu kullanım şekli contains formatında çalışır.
# Örneğin sadece adında calculate kelimesi geçen testleri çalıştırmak istersek
# cargo test calculate dememiz yeterli olur
cargo test should_calculated_player_score_positive
# Fonksiyonlardan terminale basılan çıktıları test sırasında da görmek istiyorsak,
# (ki normalde görünmezler)
# test komutunu aşağıdaki şeklinde kullanmamız gerekir.
cargo test -- --show-output
factorial sonrası geliştirilen diğer örneklerde cargo aracından yararlanılmıştır.
factorial programına ait örnek ekran çıktısı
lucky _ number isimli sayı tahmin oyunundan iki görüntü
fundamentals isimli örnekte immutable atam ihlaline ait çalışma zamanı hatası
ownership örneğindeki barrowed move olayı
errorhandling örneğinden bir kuple panic! havası
errorhandling örneğinden Result<T,E> ile olayı kontrol altında tutmaya çalışma
traits örneğinden bir ekran görüntüsü
lifetimes örneğinde derleme zamanı hatasının görüntüsü
testing isimli unit test kullanımı örneğinden görüntüler
testlerden biri başarılı diğeri değil durumu
belli bir test maddesini çalıştırdığımız durum
/// ile kullanım talimatlarını eklediğimizdeki durum
reader isimli dosya içeriğini parse edip ürün vektörüne dönüştüren uygulamadan bir hatıra
closures2 kodundan örnek LINQ sorgusuna ait ekran görüntüsü
iterators2 örneğindeki test sonuçlarına ait bir görüntü
Kendi struct türümüzdeki alanları for döngüsü ile gezebildiğimiz own<>iterator uygulamasından bir görüntü_
Fearless Concurrency örneğinin ilk çalışması(Ana thread sonlandığı için devam eden diğer thread'lerin de sonlandığı durum)
join handle örneğinden bir görüntü. Ana thread'i devam eden thread için bekletiyoruz
yine join handle'dan bir görüntü. Bu sefer devam eden iki thread için beklettik
multi join örneğinden bir kesit(move kullanımı var)
counter uygulamasından bir ekran görüntüsü
message passing örneğindeki #1 kodunun çıktısı
message passing örneğinde #3 kodunun eklenmesi sonrası çıktı
- Rust dilinde değişkenler neden varsayılan olarak immutable işaretlenir?
- factorial örneğindeki expect fonksiyonları hangi hallerde devreye girer? panic durumları bu kod parçasında nasıl ele alınır?
- lucky_number örneğindeki match kullanımlarının ne işe yaradığını bir arkadaşınıza anlatınız?
- Büyük veri yapısına sahip bir tipi mutable mı kullanmak uygundur, immutable olarak mı? Yoksa duruma göre değişir mi?
- shadowing hangi durumlarda mantıklı olabilir?
- Ne zaman array ne zaman vector kullanmak uygun olur?
- C# dilinde String atama ve metotlara parametre olarak geçme davranışları ile Rust tarafındakileri karşılaştırın.
- ownership uygulamasının aldığı derleme zamanı hatasının sebebi nedir?
- Hiçbir alan (field) içeren bir struct tanımlanabilir mi? Buna izin veriliyorsa amaç ne olabilir?
- structs örneğinde yer alan println!("{}", mouse.title); kod parçası açılırsa neden derlenmez? (Line: 18)
- Yine structs örneğinde 19ncu satırdaki kod, mouse değişkeni mut ile mutable yapılsa dahi derleme hatasına neden olacaktır. Neden?
- Bir enum yapısındaki değişkenler başka enum değişkenlerini de içerebilir mi?
- Bir vector koleksiyonunda farklı tipten elemanlar tutmak istersek ne yaparız?
- String'leri + operatörü ile birleştirirken neden & ile referans adresi kullanırız?
- collections örneğinde a_bit_off_word değişkenine siyah isimli metindeki ilk karakteri almak ve panic durumunun oluşmasını engellemek için ne yapılabilir?
- Unwinding kabiliyeti nasıl etkinleştirilir?
- traits isimli örnekte yer alan Action içerisindeki initialize metodunun Hyperlink fonksiyonu için kullanılmasını istemezsek nasıl bir yol izlememiz gerekir
- lifetimes isimli programdaki #1 örneğinde oluşan derleme zamanı hatasını nasıl düzeltebilirsiniz?
- Bir fonksiyon birden farklı generic lifetime parametreleri kullanabilir mi?
- Bir test fonksiyonu sonuç dönebilir mi?
- Ne zaman normal fonksiyon ne zaman closure?
- iterators2 örneğinde yer alan Game struct'ı için neden #[derive(PartialEq, Debug)] niteliklerini uyguladık?
- cons list kullanmamızı gerektirecek bir durum düşünün ve tanıdığınız bir Rustacean'a bunu anlatın.
- Rc kullanmamızı gerektirecek en az bir senaryo söyleyebilir misiniz?
- Arc (Atomic Reference Counting) tipi hangi amaçla kullanılır.
- Bir struct değişkenini match ifadesi ile kullanabilir miyiz?
- lucky_number örneğindeki cpm işlem sonucunu match yerine if blokları ile tesis ediniz.
- luck_number örneğinde loop döngüsü kullanmayı deneyiniz
- Bir kitabı birkaç özelliği ile ifade eden bir struct yazıp, bu kitabın fiyatına belirtilen oranda indirim uygulayan metodu geliştiriniz (Metot, impl bloğu ile tanımlanmalı)
- mercury isimli kütüphaneyi başka bir rust uygulamasında kullanabilir misiniz? Nasıl?
- Bir String içeriğini tersten yazdıracak fonksiyonu geliştiriniz? (rev kullanmak yasak)
- error_handling örneğinde 69ncu satırda başlayan ifadede i32'ye dönüşemeyen vector değerlerini hariç tuttuk. Geçersiz olan değerleri de toplayıp ekrana yazdırabilir misiniz? (ipucu : partition fonksiyonu)
- İki kompleks sayının eşit olup olmadığını kontrol eden trait'leri geliştiriniz
- Iterator trait'ini yeniden programlayarak Fibonnaci sayı dizisini belli bir üst limite kadar ekrana yazdırmayı deneyiniz
- Fizz Buzz kod katasını Rust ile TDD odaklı geliştirin
- reader uygulamasındaki akış kodlarını ayrı bir kütüphaneye alın
- .Netçiler!!! Birkaç LINQ sorgusunu closure'ları kullanarak icra etmeye çalışın
- Closures örneğinde yer alan get_fn fonksiyonunu inceleyin. Sizde farklı bir senaryo düşünüp geriye koşula göre fonksiyon döndüren ama Fn yerine FnMut trait'ini ele alan bir kod parçası yazmayı deneyin.
- iter fonksiyonu üstünden örneğin 1den 100e kadar olan sayılardan sadece kendisi ve 1 ile bölünülebilenleri (asal olanları) elde etmeye çalışın,
- hof örneğinde 28nci satırdaki filter fonksiyonuna bakın. Burada calc fonksiyonunu çağırmadan aynı hesaplamayı yaptırın
- M:N ve 1:1 thread modelleri nedir, araştırınız? Öğrendiklerinizi bir arkadaşınızla paylaşıp konuyu tartışarak pekiştiriniz.
- counter uygulamasını genişletelim. En az 20 paragraftan oluşan bir word dokümanı hazırlayın. Herbir paragraf için ayrı bir thread çalıştırın. Herbir thread ilgili paragrafta bizim söylediğimiz kelimelerden kaç tane geçtiğini case-sensitive veya case-insensitive olarak hesaplasın.
- _ ve .. operatörlerinin kullanım alanları nerelerdir, araştırıp deneyiniz.