ana sayfagithubtwitteristatistikrss

Rust günlüğü.

Rust beni neden heyecanlandırıyor? 03.07.2020

Kod yazmaya 2000 yılında C ile başladım. Yıl 2020 oldu ve bir programlama dili beni yeniden heyecanlandırıyor. Bu duyguyu en son 2012 yılında Scala ile yaşamıştım.

Aslında Commodore 64 ile büyüyen nesile yaş itibariyle dahil olsam da sosyal nedenlerle bilgisayarla üniversitede tanıştım. Birinci sınıfın ikinci döneminde, programlama dersinde C görmeye başlamıştık. O ara eve bilgisayar alındı. İlk kodumu yazıp çalıştırdıktan sonra aldığım hazzı ve “Ben bunun için doğmuşum” dediğimi dün gibi hatırlıyorum. Sonrasında kişisel projelerime C++ ile devam ettim. 2004 yılında yaptığım bitirme projem, o zamanlar çok yeni kavramlar olan internet üzerinden sıkıştırılmış görüntü ve ses iletimi üzerineydi ve projeyi Borland C++ Builder ile yazmıştım. Hatta 2006-2008 arası geliştirdiğim ilk açık kaynak projem termula2x için de C++ kullanmıştım.

Profesyonel kariyerimde ise ilk birkaç ay C# yazdıktan sonra 2006 yılından itibaren Java kullanmaya başladım. 2013 yılından beri ağırlıklı olarak Scala yazıyorum ama iki dili de hemen hemen her gün kullanıyorum. Ara ara javascript, coffeescript, php, python, perl yazdığım da oldu ama bu dilleri hiçbir zaman uzun süreli ve derinlemesine öğrenerek kullanmadım.

Hem kişisel hem de profesyonel projelerimde kullandığım dillere bakınca elimin altında her zaman statik bir tip sistemi olduğu kolayca görülüyor. An itibariyle en çok sevdiğim ve kendimi en rahat hissettiğim dil olan scala da çok güçlü bir tip sistemine sahip. Bunun yanında fonksiyonel programlamadan gelen immutability, referential transparency, higher order functions, pattern matching gibi kavramlar da günlük programlama deneyimimin bir parçası.

Son 7 yıldır mikroservis mimarisi ile yüksek yük altında çalışan backend servisler geliştiriyorum. Dolayısıyla parallelism, concurrency, distributed computing kavramları günlük işlerim arasında. Bu noktada Scala ve ekosisteminden gelen dil ve kütüphane bileşenleri hayatımı çok kolaylaştırıyor. Asenkron işlemler backpressure desteğiyle elimin altında. Stream processing 5 dakikada yazabildiğim fluent api çağrılarından ibaret. Özetle kullandığım araçlar açısından keyfim neredeyse yerinde.

Yukarıda toz pembe bir tablo çizdim ama elbette her şey mükemmel değil. Örneğin scala compiler hala çok yavaş. Elimizdeki çok güçlü makinalara ve optimize edilmiş derleyiciye rağmen yavaş. Evet, scala compiler bizim için çok fazla iş yapıyor. Production’da karşılaşılabilecek hataları en aza indiriyor. Fakat bu yavaşlık geliştirici verimliliğini mutlaka etkiliyor. Dürüst olmak gerekirse daha hızlı bir derleyiciye hayır demezdim.

Diğer bir sorun da yıllardır optimize edilmesine rağmen hala sorun çıkartan garbage collector. Scala ile yazdığım kod JVM üzerinde çalışıyor. JVM tuning zaten bir tür sihirbazlık. Ne yaparsanız yapın gc latency sizi bir yerde yakalıyor. Garbage collection olmayan ama bellek yönetiminde aynı güvenceyi sağlayan bir ortama hayır demezdim.

Dediğim gibi scala ile yazdığım kod java virtual machine üzerinde çalışıyor. Bu da hem sisteme fazladan araçların kurulması hem de gereksiz işlem yükü anlamına geliyor. Gerçi artık scala ve java için native kod üreten araçlar mevcut ama hepsi henüz çok erken safhada. Kararlı bir şekilde native kod üreten bir programlama diline hayır demezdim.

Ve insan Rust’ı yarattı…

Garbage collector olmadan otomatik bellek yönetimini garanti eden, native kod üreten görece hızlı bir derleyiciye sahip, kolayca asenkron kod yazmaya imkan veren, gelişmiş bir tip sistemi ve genericler de dahil olmak üzere yukarıda saydığım dil özelliklerine sahip bir programlama dili Rust. Daha baştan paket yönetimini düzgünce çözmesinin yanında gelişmiş araç ve dokümantasyon desteği ile giriş aşaması da çok kolay. Bu yüzden her gün yavaş yavaş Rust öğrenmek bana çok keyif veriyor.

Bu günlükte bir yandan ben Rust öğrenmeye devam ederken her sabah kısa kısa ilgimi çeken, hoşuma giden şeylerden bahsedeceğim. Bu formatı Sağlıklı Yaşam yazılarında kullanmıştım ve çok beğenmiştim. Yarın görüşmek üzere👋

println! neden asabi? 04.07.2020

Cargo, Rust’ın paket yöneticisi. Yeni proje oluşturmak, projeyi build etmek, test etmek, bağımlılıkları indirmek gibi işlerin hepsini cargo ile yapıyoruz. Örneğin yeni bir proje oluşturmak için aşağıdaki komut kullanılıyor:

cargo new rust-gunlugu

Bu komut ile rust-gunlugu dizini altında basit bir “Merhaba Dünya!” şablonu elde ediyoruz.

├── Cargo.toml
└── src
    └── main.rs

main.rs dosyası içindeki main fonksiyonu uygulamamızın giriş noktası. Oluşan koda bakarsak uygulamamızın “Hello World!” diye çığlık attığını görüyoruz.

fn main() {
    println!("Hello, world!");
}

Hemen hemen tüm programlama dillerinin buna benzer “Merhaba Dünya!” uygulaması vardır ve bunu ! işareti ile çığlık atarak yapar. Fakat Rust println! çağrısında bile bize bağırıyor. Bunun nedeni println!‘in aslında bir fonksiyon değil makro olması. Makrolar derleyici tarafından Rust koduna genişletiliyorlar ve sonrasında tekrar derleniyorlar.

println! makrosunun derlendiği kodu görebilmek için cargo-expand projesinden faydalanacağım. Aşağıdaki komut ile hızlıca yükleyebiliyoruz.

cargo install cargo-expand

Bu komutu kullanabilmek için ayrıca nightly rust’a ihtiyacımız var. Yüklemek için Rust kurulumu ile gelen rustup komutundan faydalanıyoruz.

rustup toolchain install nightly

Artık her şey hazır. cargo expand komutunu çalıştırdığımızda 3 satırlık main fonksiyonunun aslında çok daha karmaşık bir Rust koduna derlendiğini görüyoruz.

fn main() {
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(
            &["Hello, world!\n"],
            &match () {
                () => [],
            },
        ));
    };
}

println! içine format string vererek olayı biraz daha karmaşık hale getirelim.

fn main() {
    let name = "Fehmi Can Saglam";
    println!("Hello, {}!", name);
}

Yine cargo expand ile oluşturulan Rust koduna bakalım.

fn main() {
    let name = "Fehmi Can Saglam";
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(
            &["Hello, ", "!\n"],
            &match (&name,) {
                (arg0,) => [::core::fmt::ArgumentV1::new(
                    arg0,
                    ::core::fmt::Display::fmt,
                )],
            },
        ));
    };
}

Oluşan bu karmaşık kodları kendimiz yazmamak için println!‘in bize bağırmasına katlanmak zorundayız🙂 Yarın görüşmek üzere👋

std::convert dostumuzdur 05.07.2020

Bugün bahsedeceğim konuya Pascal Hertleif, Writing Idiomatic Libraries in Rust konuşması ile ilham verdi. Youtube’da ciddi miktarda Rust konulu içerik olduğunu farkettiğimden beri vakit buldukça izliyorum ve daha sonra yazmak üzere not alıyorum. Bu yüzden birçok yazımın kaynağı muhtemelen bu videolar olacak.

std::convert modülündeki traitler bir tipten başka bir tipe dönüşüme olanak sağlıyorlar. Örneğin From trait ile bir değerden başka bir değere dönüşüm yapabiliyoruz. Hem de bu traiti gerçeklediğimizde Into trait de bizim için otomatik olarak gerçeklenmiş oluyor. Bu şekilde anlatınca hiçbir şey ifade etmemiş olabilir. Küçük bir örnekle açıklamaya çalışayım.

Point isminde bir veri yapımız olsun, bir noktanın x ve y koordinatlarını tutsun.

struct Point {
    x: i32,
    y: i32,
}

Yeni bir nokta tanımlamak için normalde aşağıdaki kodu kullanırız.

let p = Point { x: -1, y: 1 };

Diyelim ki elimizde noktanın koordinatlarını tutan iki elemanlı bir dizi var ve bu diziden Point tipinde bir değer yaratmak istiyoruz. Bu durumda From traitini gerçeklememiz yeterli oluyor.

impl From<[i32; 2]> for Point {
    fn from(coords: [i32; 2]) -> Self {
        Point { x: coords[0], y: coords[1] }
    }
}

Rust’in tip sistemi [i32; 2] şeklinde 2 elemanlı integer dizi tanımına izin veriyor. Harika değil mi? Artık iki elemanlı bir diziden aşağıdaki şekilde bir nokta yaratabiliriz.

let p = Point::from([-1, 1]);

From traitini gerçekleyince Into traitinin de otomatik olarak gerçeklendiğinden bahsetmiştim. Böylece aşağıdaki şekilde de bir nokta yaratabiliyoruz. Bence bu hali gerçekten çok temiz oldu.

let p: Point = [-1, 1].into();

Benzer dönüşümleri FromStr ile de yapabiliyoruz. Bu traiti implement edince string üzerinde parse metodunu çağırarak istediğimiz tipi elde edebiliyoruz. Örneğin renk belirten bir enum tanımlayalım.

enum Color {
    Red,
    Green,
    Blue,
}

Color için FromStr traitini gerçeklersek “blue”.parse::<Color>() kodu ile stringden Color tipine dönüşüm mümkün oluyor.

impl FromStr for Color {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "red" => Ok(Color::Red),
            "green" => Ok(Color::Green),
            "blue" => Ok(Color::Blue),
            _ => Err(())
        }
    }
}

Parse işlemi elbette bazen başarısız olabilir. Ben bu örneği basit tutmak adına başarısızlık durumu için düzgün bir hata dönmek yerine unit adı verilen () döndüm.

Yarın başka bir konu ile görüşmek üzere👋

Rust ile basit postfix parser 06.07.2020

Reverse Polish notation(RPN) veya diğer adıyla postfix notation hesap makinalarında çokça kullanılan bir ifade biçimi. Hepimizin aşina oldugu infix notasyonunda işlem işlenenden önce gelir. Örnegin 3 + 4 ifadesi infix biçimdedir. Postfix notasyonunda ise işlem işlenenden sonra gelir. Aynı ifade postfix notasyonunda 3 4 + biçiminde yazılır. Postfix notasyonun en güzel tarafı parantezlere ihtiyaç duymamasıdır. 3 * (4 + 8) / (6 - 2) gibi karmaşık bir infix ifadesi postfix notasyonunda 3 4 8 + * 6 2 - / şekline dönüşür.

Bu yazıda basit bir postfix ifade parser yazmaya çalışacağım. İfadeler bize infix notasyonda geliyorsa elbette önce postfix notasyona dönüşüm gerekir. Ama ben işi basit tutmak adına bu adımı atlıyorum. Hatta ifadenin string olarak değil de işlem(operation) ve işlenen(operand) şeklinde tokenlarına ayrılmış olarak elimizde oldugunu varsayıyorum. Böylece işin özündeki veri yapılarına odaklanabileceğiz.

Yukarıda söz ettiğim gibi postfix notasyonda iki çeşit token var. Biri yapılacak işlemi belirten operator, diğeri de üzerinde işlem yapılan sayı yani operand. Yalnızca 4 işlemi destekleyen hesap makinamızda 4 çeşit operator var ve bunları bir enum ile ifade edebiliriz.

enum Operator {
    Add, // +
    Sub, // -
    Mul, // *
    Div, // /
}

Operand ve operator tokenlarını da yine bir enum içinde ifade edebiliriz. Böylece operator ve operand aynı türün farklı örnekleri gibi davranacaklar. Bunu bir tür inheritance gibi de düşünebiliriz.

enum Token {
  Op(Operator),
  Operand(i32) // Hesap makinamız yalnızca tamsayıları destekliyor.
}

Bu şekilde yapınca bir token dizisine hem operator hem de operandları koymamız mümkün oluyor.

use Token::*;
use Operator::*;
// 3 4 8 + * 6 2 - /
let tokens = [
    Operand(3),
    Operand(4),
    Operand(8),
    Op(Add),
    Op(Mul),
    Operand(6),
    Operand(2),
    Op(Sub),
    Op(Div),
];

Artık elimizde tokenlarımız olduğuna göre bir stack yardımıyla bu tokenları işleyip ifadenin sonucunu bulabiliriz. Rust dokümantasyonuna göre Vector veri yapısı birçok iş için ideal ve stack de bunlardan biri.

let mut stack = Vec::new();

Algoritmamız gayet basit. Tokenlarımızı soldan sağa işlerken eğer bir operanda denk gelirsek bunu stack’e iteceğiz. Operator geldiğinde stack’ten iki operand çekip operatorün belirttiği işlemi yaptıktan sonra tekrar stack’e iteceğiz. Tüm tokenlar bittiğinde stack’te kalan son sayı da tüm ifadenin sonucu olacak.

for token in tokens.iter() {
    match token {
        Operand(number) => {
            stack.push(*number)
        }
        Op(operator) => {
            let right = stack.pop().expect("Sağ operand bulunamadı!");
            let left = stack.pop().expect("Sol operand bulunamadı!");
            match operator {
                Add => stack.push(left + right),
                Sub => stack.push(left - right),
                Mul => stack.push(left * right),
                Div => stack.push(left / right),
            }
        }
    }
};

println!("Sonuç: {}", stack.pop().unwrap());

stack.pop() eğer stack boş ise bir token dönemeyeceği için Option tipinde bir değer dönüyor. Birçok programlama dilinde mevcut olan bu tipten başka bir yazıda bahsedeyim. Uygulamamıza gönderilen ifade geçersiz bir ifade ise stack’ten operand çekmeye çaliştığımızda None elde edeceğiz. None durumu için özel bir işlem yapmaktansa uygulamanın hata verip çıkmasını tercih ettim. Bu amaçla expect metodunu kullanıp daha düzgün bir hata mesajı vermeye çalıştım.

Yarın başka bir konu ile görüşmek üzere👋

FromIterator ile istatistik üretmek 07.07.2020

FromIterator bir iterator’dan kendi tanımladığımız bir tipin nasıl üretilebileceğini belirtiyor. Kendi tanımladığımız tip için bir kısıtlama yok. Dokümantasyonda bir vektörü sarmalayan MyCollection isminde bir struct örneği verilmiş. Doğrudan collection içermeyen bir örnek bana daha ilgi çekici geldi ve collection istatistiklerini tutan bir tip yazmak istedim.

#[derive(Debug)]
struct Stats {
    count: i32,
    sum: i32,
    avg: f32,
}

Tamsayılar içeren bir collection hakkında eleman sayısı, toplam ve ortalama gibi basit istatistikler tutan veri yapısını yukarıdaki gibi tanımladım. #[derive(Debug)] satırı ile compiler bizim için Debug traitini gerçekliyor ve println! ile ayrıntılı çıktı basabilmemizi sağlıyor.

Şimdi de Stats tipi için FromIterator traitini implement edelim. Basitçe iterator üzerinde dolaşacağız ve istatistikleri üretip Stats içinde döneceğiz.

impl FromIterator<i32> for Stats {
    fn from_iter<I: IntoIterator<Item=i32>>(iter: I) -> Self {
        let mut count = 0;
        let mut sum = 0;
        for val in iter {
            count += 1;
            sum += val;
        }
        Stats{
            count,
            sum,
            avg: sum as f32/ count as f32,
        }
    }
}

Artık bir tamsayı listesinden Stats nesnesine dönüşüm yapabiliriz. Aşağıdaki örnek 1 ve 4 kapalı aralığındaki sayılar için istatistik hesaplıyor.

let stats: Stats = (1..=4).into_iter().collect();
println!("{:?}", stats)

Debug traiti bizim için implement edildiğinden println! çıktısı epeyce ayrıntılı oluyor.

Stats { count: 4, sum: 10, avg: 2.5 }

Yarın başka bir konu ile görüşmek üzere👋

Option tipi ile 1 milyar dolar cebimizde kalsın 08.07.2020

1965 yılında bizi null referanslar ile tanıştıran Tony Hoare, 2009 yılında yaptığı şu konuşmada null referansları milyar dolarlık hata olarak nitelemişti. Bu sorunu çözmek için, başka bir deyişle null pointer hatası almamak için birçok dil Option tipi sunuyor. Option tipi aslında başka bir değeri sarmalayarak var veya yok durumlarını ifade etmeye yarıyor. 2012 yılında Scala yazmaya başladığımda bu tip mevcuttu. Java 8 ile 2014 yılında java geliştiriciler de -daha az yetenekli olsa bile- bu tiple tanıştılar. Rust dilinde Scala’dakinin yeteneklerine çok benzer bir Option tipi mevcut. Bu yazıda Option tipinin nasıl kullanılabileceğinden bahsetmeye çalışacağım.

Rust’ta Option tipi aşağıdaki gibi bir enum olarak tanımlanmış. Eğer bir değer mevcut değilse None, mevcutsa Some ile ifade ediliyor.

pub enum Option<T> {
    None,
    Some(T),
}

Rust null referanslara izin vermiyor. Buna izin veren dillerde derleyici bizi null referans kontrolü için zorlamaz. Fakat Option tipi ile işlem yaparken derleyici bizi bu kontrolü yapmak zorunda bırakır. İlk kullanım örneği olarak dokümantasyonda verilen partial function’dan başlamak istiyorum. Bu tür fonksiyonlar tüm girdiler için bir çıktı üretemezler. Mesela bölme işlemi yapan fonksiyonumuz bölünen ve böleni parametre olarak alıyor olsun. Bölme işlemi bölen 0 olduğunda tanımsız olduğundan fonksiyonumuz da bu girdi için tanımsızdır. Bu durumu ifade edebilmek için fonksiyonun dönüş tipini Option yapabiliriz ve bölen 0 olduğunda None dönebiliriz.

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

Bu noktadan sonra divide fonksiyonunu çağıran kod None durumunu kontrol etmek zorunda. Bu kontrol için en temel yöntem pattern matching. Java’da pattern matching olmadığından Option tipi oldukça kullanışsız geliyordu. Rust, Scala’ya benzer şekilde bu özelliği sağladığından beni gayet mutlu etti.

// divide fonksiyonu bir Option dönüyor.
let result = divide(2.0, 3.0);

// Sonucu okuyabilmek için pattern matching kullanıyoruz.
match result {
    // Bölme işlemi başarılı
    Some(x) => println!("Sonuç: {}", x),
    // Bölme işlemi geçersiz
    None    => println!("0 ile bölünemez!"),
}

Rust, Scala kadar fonksiyonel programlamadan bahseden ve category theory’yi gözümüze sokan bir dil değil. Ortalama bir Scala geliştirici Option tipinin bir monad olduğunu bilir ve monad özelliklerini sağlamasını bekler. İşin güzel tarafı Rust monad kelimesinden hiç bahsetmeden bu özellikleri bize sunuyor. Bunlardan kısaca bahsetmeye çalışayım.

map metodu ile eğer option içinde bir değer varsa bu değere bir fonksiyon uygulayabiliyoruz. Örneğin bir string mevcut olduğunda uzunluğunu hesaplayan bir kod yazalım.

let maybe_name = Some(String::from("Fehmi Can Saglam"));
let maybe_len: Option<usize> = maybe_name.map(|name| name.len());

Yukarıdaki örnekte maybe_len değeri ancak maybe_name değeri mevcut olduğunda var olabileceğinden bu değerin tipi de bir Option oluyor. Eğer Option tipi kullanmıyor olsaydık name null olduğu durumda name.len() çağrısında null pointer exception alacaktık.

and_then metodu Scala’daki flatmap işlevini görüyor. Örneğin kullanıcı bilgisini aldıktan sonra adres bilgisini başka bir fonksiyon yardımıyla okuyan bir kod yazalım. Kullanıcı bilgisi bir şekilde okunamayacağı gibi adres bilgisi de mevcut olmayabilir. Tüm bu durumları Option tipi ve and_then metodu ile ifade edebiliriz.

struct User {
    id: i32,
    name: String
}

struct Address {
    user_id: i32,
    city: String,
    country: String,
}

fn get_user(id: i32) -> Option<User> {}

fn get_address(user_id: i32) -> Option<Address> {}

// Eğer 1 id'li kullanıcı mevcutsa, bu kullanıcının adresini yüklüyoruz.
let maybe_address: Option<Address> = get_user(1).and_then(|user| get_address(user.id));

Son olarak da or_else metodundan bahsetmek istiyorum. Kullanıcı bilgisini önce cache’ten, burada mevcut değilse veritabanından yüklemek istediğimizi düşünelim.

fn get_user_from_cache(id: i32) -> Option<User> {}

fn get_user_from_db(id: i32) -> Option<User> {}

let maybe_user: Option<User> = get_user_from_cache(1).or_else(|| get_user_from_db(1));

or_else metodunun lazy olması bu noktada önem arz ediyor. get_user_from_db fonksiyonu yalnızca kullanıcı cache’te mevcut değilse çağrılacak.

Ben de yeni yeni öğrendiğim için şu an daha kısa yolları bilmiyor olabilirim. Bunları zamanla öğrendikçe paylaşmaya çalışacağım. Yarın başka bir konu ile görüşmek üzere👋

? operatörü ile hataları dönmek 09.07.2020

Hata bu işin fıtratında var🙂 Rust uygulamada oluşabilecek ve bir şekilde tepki verilebilecek hataları ifade etmek için Result tipini kullanıyor. Bu tip Scala geliştiricilerin aşina olduğu Either ve biraz da Try tiplerine çok benziyor. Hataları bir tip ile ifade etmek dün bahsettiğim Option tipi ile null referansları ifade etmekle hemen hemen aynı. Bu tür durumları tip sistemi yardımıyla ifade ettiğinizde derleyici gözden bir şey kaçırmanıza engel oluyor.

Result da Option gibi bir enum olarak tanımlanmış ve Ok ile Err olmak üzere iki varyanttan oluşuyor. Ok varyantı ile başarılı durumda sonucu dönerken Err varyantı ile hatayı dönüyoruz.

enum Result<T, E> {
   Ok(T),
   Err(E),
}

Dünkü yazımda get_user ve get_address isminde Option dönen iki fonksiyon yazmıştım. Bunları Result tipine örnek vermek için değiştireceğim. Elbette örneği yine çok basit tutuyorum ama işin özünü gösterebileceğimi umuyorum. İki fonksiyonda da gelen id 1’den küçük mü kontrolü yaparak küçükse hata ve hata içinde bir mesaj dönüyorum.

fn get_user(id: i32) -> Result<User, String> {
    if id < 1 {
        Err(String::from("id 1'den küçük olamaz!"))
    } else {
        Ok(User { id, name: String::from("Fehmi Can Saglam") })
    }
}

fn get_address(user_id: i32) -> Result<Address, String> {
    if user_id < 1 {
        Err(String::from("user_id 1'den küçük olamaz!"))
    } else {
        Ok(Address { user_id, city: String::from("Berlin"), country: String::from("Almanya") })
    }
}

Hatırlarsanız bu iki fonksiyon çağrısı birbirine bağımlı idi. Önce kullanıcıyı yükledikten sonra işlem başarılı ise kullanıcı adresini yüklememiz bekleniyor. Bu işlemi yapan üçüncü bir fonksiyon tanımlayalım. Bu fonksiyon hata durumlarını kendisi ele almaktansa olduğu gibi dönsün. İlk yöntem olarak pattern matching kullanacağım.

fn get_user_address(id: i32) -> Result<Address, String> {
    let user = match get_user(id) {
        Ok(user) => user,
        Err(e) => return Err(e),
    };

    match get_address(user.id) {
        Ok(address) => Ok(address),
        Err(e) => Err(e)
    }
}

Tam olarak neye ihtiyacımız olduğunu gösterebilmek için yukarıdaki kodda işi epeyce uzattım. Yine pattern matching kullanarak aynı işi aşağıdaki gibi de yapabilirdik.

fn get_user_address(id: i32) -> Result<Address, String> {
    match get_user(id) {
        Ok(user) => get_address(user.id),
        Err(e) => Err(e)
    }
}

Bu kodu ? operatörü ile daha da sadeleştirmemiz mümkün. Bu operatör başarılı durumda ilgili değeri dönerken hata durumunda içinde bulunduğu tüm fonksiyondan hatayı return ediyor.

fn get_user_address(id: i32) -> Result<Address, String> {
    let user = get_user(id)?;
    get_address(user.id)
}

Birbirine bağımlı 3 fonksiyonumuz olsaydı ? operatörünün sağladığı kolaylık daha da görünür olurdu. Çünkü bu durumda iç içe match satırları yazmak yerine tek bir satır eklemek yeterli olacaktı.

Yarın başka bir konu ile görüşmek üzere👋

Rust ile ifade özgürlüğü 10.07.2020

Scala’da her şey bir expression(ifade). Bunun faydalarını gördükten sonra yalnızca statement’lar ile çalışan diller beni çok zorlamaya başladı. Örneğin Java yazarken bu durumdan oldukça rahatsızım. Expression bir değer üretir ve dönerken, statement aslında bir tür durum veya emir belirtir. Dolayısıyla statement’lar imperative dillerin, expression’lar da fonksiyonel dillerin temel bileşenleridir. Expression ve statement farkını örneklerle anlatmaya çalışayım.

Rust’ta fonksiyonun veya bir bloğun son satırındaki ifade fonksiyonun dönüş değeridir. Bu nedenle return yazmanıza gerek yoktur.

fn square(a: i32) -> i32 {
    a * a
}

İfadeleri b = a * a şeklinde bir değişkene atayabiliriz. Aynı şeyi Rust’ta if/else ile de yapabiliriz.

let result = if factor >= 0.0 {
    score * factor
} else {
    score / factor
};

Verdiğim örnekte result değişkeni immutable ve factor parametresine göre farklı bir değere sahip olması gerekiyor. Bunu bir statement ile yapmaya çalışsaydık değişkeni her durum için tekrarlamamız gerekecekti.

let result;
if factor >= 0.0 {
    result = score * factor
} else {
    result = score / factor
}

Benzer şekilde if/else ifadesini fonksiyonun dönüş değeri olarak da kullanabiliriz.

fn apply_factor(score: f64, factor: f64) -> f64 {
    if factor >= 0.0 {
        score * factor
    } else {
        score / factor
    }
}

Rust’ta pattern matching de bir ifade ve match sonucu immutable bir değişkene atanabilir.

let user_name = match get_user(id) {
    Ok(user) => Ok(user.name),
    Err(e) => Err(e)
};

Benzer şekilde match ifadesi bir bloğun veya fonksiyonun dönüş değeri olabilir.

fn get_user_name(id: i32) -> Result<String, String> {
    match get_user(id) {
        Ok(user) => Ok(user.name),
        Err(e) => Err(e)
    }
}

Bir dil statement’lardan oluşuyorsa o dil mutability ve side effect üzerine kurulmuş demektir. Oysa biz hatasız uygulamalar yazmak için bunlardan kaçınıyoruz. Rust, expressionlar ile kod yazmamıza imkan veriyor. Bu yüzden hatasız uygulamaları ifade özgürlüğümüz var. Yarın başka bir konu ile görüşmek üzere👋

Option tipini iterator gibi kullanmak 11.07.2020

Daha önce Option tipinden ve özellikle null referans hatasına karşı bizi nasıl koruduğundan bahsetmiştim. Bugün de Option tipini 0 veya 1 elemanlı bir liste gibi düşünüp diğer koleksiyon tipleriyle nasıl birleştirebileceğimize değinmek istiyorum.

Konumuza geçmeden önce bu paragrafta başka bir konudan bahsetmek istiyorum. Takip ettiğiniz üzere sekiz gündür Rust hakkında bir şeyler karalıyorum. Şu anda da dokuzuncu yazıyı yazıyorum ve Türkçe yazdığım teknik yazılarda terimleri yazarken çok zorlanıyorum. Örneğin ilk paragrafta beni zorlayan terimler type, list, iterator, collection ve composition oldu. Bu konunun birçok yerde tartışıldığını biliyorum ancak ortada benim bildiğim kadarıyla bir sonuç yok. Diğer yandan bu yazıyı İngilizce de yazabilirim ama bu bana Türkçe yazmak kadar keyif vermiyor. Çünkü yazılı ifade benim için bir tür oyun. Ana dilimde aynı şeyi farklı şekillerde ifade etmeye çalışmak, kelime tekrarı olmadan cümleler kurmak, yazım kurallarına göz ucuyla yeniden bakmak bence çok zevkli. İtiraf etmem gerekirse “ana dili” ayrı mı yoksa bitişik mi yazılır diye bir baktım ve Nazım Hikmet’in şu güzel cümlesiyle karşılaştım: “İnsan tehlike karşısında ancak ana diliyle feryat edebiliyor”. Elbette işin bir de okuyucu kısmı var. Ben yaş itibariyle İngilizce eğitimin görece çok daha kaliteli olduğu bir dönemde yetiştim. Fakat son dönemde yetişen nesil takip edebildiğim kadarıyla bu konuda şanssız. Bana iterator ya da collection terimleri çok doğal gelirken herkeste aynı algı var mı bilemiyorum. İşin doğrusu bu konudaki yorumlarınızı beklemiyorum🙂 Yalnızca bir durumu bir sıkıntımı ifade etmek istedim. Amacım bu konuda bir tartışma başlatmak değil.

Şimdi konumuza iterator ile dönüyorum. Elimizde bir tür koleksiyon varsa ve koleksiyonun elemanları üzerinde işlem yapmak istiyorsak karşımıza iterator çıkıyor. Rust’ta belki en sık kullanılan koleksiyon tipi olan vektör ile bir örnek vereyim.

let vec = vec![0, 1, 2, 3];

for v in vec.iter() {
    println!("{}", v);
}

Option tipi de aslına bakarsak None veya Some olma durumuna göre 0 veya 1 elemanlı bir koleksiyon. Bu yüzden yukarıdaki kodu hemen hemen aynı şekilde Option tipinde bir değer ile de yazabiliriz.

let maybe_name = Some("Fehmi Can Saglam");

for v in maybe_name.iter() {
    println!("{}", v);
}

Mevcut haliyle bu döngü yalnızca 1 kez çalışıyor. maybe_name değişkeni None olsaydı bu döngü hiç çalışmayacaktı. Madem ki Option tipinden bir iterator elde edebiliyoruz, diğer koleksiyon tipleri ile birlikte nasıl kullanabiliriz bir bakalım. İlk vereceğim örnek bir vektör ile option’ı zip metodu ile birleştirmek olacak.

let maybe_name = Some("Fehmi Can Saglam");
let vec = vec![0, 1, 2, 3];

for v in maybe_name.iter().zip(vec.iter()) {
    println!("{:?}", v);
}

// Çıktı
// ("Fehmi Can Saglam", 0)

Her ne kadar vektör içinde 4 eleman olsa da diğer iterator 1 elemanlı olduğu için zip metodu 1 elemanlı bir iterator üretti. Şimdi de chain metodu ile bir vektör ve bir option’ı uç uca bağlayalım.

let vec = vec![0, 1, 2, 3];
let maybe_value = Some(42);

for v in vec.iter().chain(maybe_value.iter()) {
    println!("{}", v);
}

// Çıktı
// 0
// 1
// 2
// 3
// 42

Bu örnekte maybe_value değeri None olsaydı yalnızca ilk vektör üzerinde dönmüş olacaktık. Böylece yazılımda soyutlamanın önümüzde ne gibi imkanlar açabildiğini görmüş olduk. Yarın başka bir konu ile görüşmek üzere👋

if/let ve while/let ile türlü oyunlar 12.07.2020

Bazı durumlarda pattern matching gereğinden fazla karmaşık olabiliyor ve bu durumları if let veya while let ile basitleştirebiliyoruz. Her zaman olduğu gibi buraya dokümantasyonu kopyalamaktansa kendimce ilginç bulduğum bir örneği paylaşacağım. Yine de bu iki kullanımı hatırlamak adına önce kısa kod örnekleri vereyim.

let number = Some(42);

if let Some(42) = number {
    println!("Etli ekmek değil, hayatın anlamı!");
}

Yukarıdaki kod parçasında number değeri yalnızca 42 olduğunda bir iş yapmak istiyoruz. Pattern matching kullandığımızda tüm durumları ele almamız zorunlu olduğundan bu kod gereksiz yere aşağıdaki gibi uzayacaktı.

let number = Some(42);

match number {
    Some(42) => println!("Etli ekmek değil, hayatın anlamı!"),
    _ => {}
}

while let ile de benzer bir örnek vereyim. 10’dan geriye doğru sayıp 0’a ulaşınca duran bir döngü yazalım.

let mut optional = Some(10);

while let Some(i) = optional {
    if i == 0 {
        optional = None;
    } else {
        println!("{}", i);
        optional = Some(i - 1);
    }
}

Yukarıdaki örnekten hareketle geri sayan bir iterator yazmak ve bunu while let ile kullanmak istiyorum. Iterator aslında next isminde bir metod içeren bir trait. Bu metod bir sonraki eleman mevcutsa Some, değilse None dönüyor.

Önce state tutmak için Counter isminde bir struct tanımlayalım.

struct Counter {
    count: u32,
}

impl Counter {
    fn new(count: u32) -> Counter {
        Counter { count }
    }
}

Şimdi de bu struct için iterator traitini gerçekleyelim. Her next çağrısında count değerini kontrol edeceğiz. count 0 ise None dönerek iterator içinde daha fazla eleman kalmadığını belirteceğiz. count 0 değilse 1 azaltıp mevcut değeri Some ile döneceğiz.

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        let curr = self.count;
        if curr == 0 {
            None
        } else {
            self.count -= 1;
            Some(curr)
        }
    }
}

Sıra gerçeklediğimiz bu iteratorü while let ile kullanmaya geldi.

let mut counter = Counter::new(5);

while let Some(val) = counter.next() {
    println!("{}", val)
}

// Çıktı
// 5
// 4
// 3
// 2
// 1

Aslında counter artık iterator olduğu için while let kullanmamıza gerek yok. Kullanımı gösterebilmek ve de Rust özellikleriyle biraz oynamak amacıyla böyle bir örnek verdim. Yukarıdaki kodu şöyle de yazabiliriz.

let counter = Counter::new(5);

for val in counter {
    println!("{}", val)
}

Dürüst olmak gerekirse for kullandığımızda counter‘ın neden mutable olmasına gerek kalmadığını henüz anlayamadım. Çünkü counter değişkeninin state’i üzerinde dönerken yine değişiyor. İlerleyen günlerde bunun nedenini anlarsam paylaşırım. Yarın başka bir konu ile görüşmek üzere👋

Rust ile json serialization 13.07.2020

Bugün Rust ekosisteminde json serialization/deserialization tarafında neler olduğuna bakmak istedim. İlk arama sonucu olarak karşıma serde çıktı. Kullanmak için önce Cargo.toml dosyasına aşağıdaki satırları eklemek gerekiyor.

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Şimdi de json’dan deserialize etmek üzere bir Server struct tanımlayalım.

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Server {
    name: String,
    memory: u64,
    ipAddresses: Vec<String>,
}

Artık bir json string’den Server elde etmek oldukça kolay.

let data = r#"
    {
        "name": "Monty Python",
        "memory": 17179869184,
        "ipAddresses": [
            "10.0.0.1",
            "10.0.0.2"
        ]
    }"#;

let server: Server = serde_json::from_str(data)?;

println!("{} is at {}", server.name, server.ipAddresses[0]);

json! macrosu ile doğrudan json oluşturabiliyoruz.

let name = "Monty Python";
let memory = 17179869184u64;

let server = json!({
    "name": name,
    "memory": memory,
    "ipAddresses": [
        "10.0.0.1",
        "10.0.0.2"
    ]
});

println!("{}", server.to_string());

Json transformation da beklediğimden çok daha kolay oldu.

let name = "Monty Python";
let memory = 17179869184u64;

let mut server = json!({
    "name": name,
    "memory": memory,
    "ipAddresses": [
        "10.0.0.1",
        "10.0.0.2"
    ]
});

server["memory"] = Value::String("16GB".to_owned());

println!("{}", server.to_string());

Daha karmaşık bir json’ı da transform etmek mümkün.

let name = "Monty Python";
let memory = 17179869184u64;

let mut server = json!({
    "name": name,
    "memory": memory,
    "ipAddresses": [
        {
            "address": "10.0.0.1"
        }
    ]
});

server["ipAddresses"][0]["address"] = Value::String("10.0.0.2".to_owned());

println!("{}", server.to_string());

Yarın başka bir konu ile görüşmek üzere👋