İçeriğe geç
academia.sh

Ders 07 / 15

Yokluğun Tiple İfadesi

Bir değerin bulunmayabileceği bir tipte taşınması ve o değeri karşılayacak yedek gövdenin — hazır mı tembel mi yazıldığına göre — değer zaten varken bile çalışıp çalışmadığının ölçülmesi.

Yayın Güncelleme

Bu derste

  • Hazır ve tembel bir varsayılan gövdenin kaç kez çalıştığını koşturarak karşılaştırır
  • Yokluk tipinin bir değerin bulunmama durumunu neden ayrı bir tip olarak taşıdığını açıklar
  • Ucuz bir varsayılan gövdede iki yazımın aynı sonucu, farklı sayıyı verdiğini gösterir
İçindekiler

İşleç Aşırı Yükleme dersi, bir işlecin her kullanımının aslında bir çağrı olduğunu ve zincirlenmiş bir toplamda islec-govdesinin iki kez çalıştığını ölçerek Trait ve Genellikler konusunu kapattı. O ders, kaynakta yazılan bir gövdenin kullanım sayısı kadar çalıştığını gösteriyordu — her kullanım kaçınılmaz bir koşumdu. Bu ders aynı soruyu bambaşka bir yere taşıyor: bir gövde, yalnız bir değer eksik olduğunda çalışıyorsa, o gövde her zaman mı çalışır, yoksa hiç çalışmayabilir mi?

Arayüzler, Hatalar ve Genellikler kursu (M09/K02) bir hatanın kendisinin bir dönüş değeri olduğunu, kontrol akışının dışına çıkmadan taşınabileceğini kurmuştu; orada ölçülen, bir hatanın nerede taşındığıydı — bildirimde değil, dönüş tipinde. Bu ders o kararı miras alıyor, ama farklı bir eksenden bakıyor: bir değerin yokluğunu karşılayan gövde — yani devreye giren yedek değer — kaç kez çalışıyor? Bu konunun (error-handling) beş dersi bu soruyu sırayla yokluğa, başarısızlığa, kısa yazıma, dönüşüme ve kurtarılamaz duruma uyguluyor; bu ilk ders yalnız yokluğa bakıyor, bir nedeni olan başarısızlık sıradaki derste geliyor.

Trait’ler, Genellikler ve Hata Yönetimi kursu boyunca aynı soru hep aynı kalıyor: kaynakta yazılan bir gövde, koşumda kaç kez çalışıyor? Trait ve Genellikler konusu bu soruyu bir davranışın (trait) gerçekleştirimlerine sordu; bu konuda soru, bir işlemin sonucunu taşıyan iki tipe — Option ve Result — ve onları izleyen kısa yazıma, dönüşüme ve kurtarılamaz duruma soruluyor. Sorunun kendisi değişmiyor, yalnız uygulandığı yer değişiyor.

Bir Tipte Yokluk

Rust’ta bir değerin bulunmayabileceği her yerde Option<T> kullanılır. Bu, önceki kursta kurulan numaralandırma (enum) düzeneğinin standart kütüphanedeki bir örneğidir: iki varyantı vardır, değeri taşıyan Some(T) ve değeri hiç taşımayan None. Bir işlevin dönüş tipi Option<i32> ise, çağıran taraf derleme zamanında bu iki durumu da ele almak zorunda kalır — istemeden None üzerinde bir değeri okumaya kalkmak, bu tipte yapısal olarak mümkün değildir.

HT1. Bu, “değer yok” durumunun ayrı bir işaret değeriyle (örneğin sıfır ya da boş bir dize) taşınmasından farklıdır. İşaret değeri, gerçek bir değerle karışabilir; None ise Some(T)in taşıdığı T tipinden tamamen ayrı bir durumdur ve derleyici ikisini asla karıştırmaz. Bu ders bu ayrımı yeniden kurmuyor — konu Sıralı ve Küme Yapıları dersinde zaten değinilmişti — yalnız Optiondan bir değer çıkarmanın kaç türlü yolu olduğuna ve bu yolların kaynakta yazılan gövdeleri kaç kez çalıştırdığına bakıyor.

Option çoğunlukla elle kurulmaz; bir aramanın doğal sonucu olarak ortaya çıkar. Sıralı bir sözlükte bir anahtarın karşılığı istendiğinde, anahtar oradaysa Some(deger), değilse None döner — arayan tarafın “anahtar var mı” diye ayrıca sormasına gerek kalmaz, arama işleminin kendisi bu bilgiyi taşıyan bir değer üretir. Aynı düzen bir dizinin sınır dışı bir indisi için de geçerlidir: doğrudan indisleme geçersiz bir indiste çöker, ama get çağrısı bir Option döner ve çağıran taraf sınır dışı durumu sıradan bir None olarak ele alır. Bu derste kullanılan örnekler bu aramaları taklit etmiyor; doğrudan Some ve None değerleri kuruyor, çünkü ölçülen şey aramanın kendisi değil, sonucundan değer çıkarma biçimidir.

Desen Eşleme dersi, bir match ifadesinin bütün dalları kapsaması gerektiğini — bütünlük ilkesini — kurmuştu. Bir Option üzerinde elle yazılan bir match de bu ilkeye uyar: Some ve None dallarının ikisi de yazılmalıdır, biri unutulursa kaynak hiç derlenmez. unwrap_or ve unwrap_or_else, bu iki dallı matchi tek bir yöntem çağrısında toplayan kısayollardır — Some(deger) => deger, None => yedek yazımının kendisi kaynakta yer almaz, ama derleyicinin ürettiği kod aynı iki dala ayrılır. Bu ders, o iki dalın kaç kez çalıştığını, match yazımını tekrar kurmadan, doğrudan bu kısayolların üzerinden ölçüyor.

Aynı bütünlük ilkesi if let yazımı için de geçerli, ama farklı bir biçimde: if let Some(deger) = secenek { ... }, yalnız Some dalını yazar ve None durumunu sessizce atlar — bu, bütünlüğü çiğnemez, çünkü if let zaten bir matchin kısmi bir biçimidir ve dilin kendisi bunu böyle tanımlar. unwrap_or ve unwrap_or_else ise tam tersi bir yerde durur: ikisi de her iki dalı da kapsar, farkları yalnız None dalının gövdesinin kaynakta nasıl yazıldığındadır. Bu derste if let ölçülmüyor, çünkü onda “çalışmayan bir dal” zaten kaynakta yok — ölçülen fark, iki dalı da yazan iki yöntemin birbirinden ne kadar farklı çalıştığı.

Hazır ve Tembel Varsayılan

Bir Option değerinden değer çekmenin en sık kullanılan iki yolu, değer yoksa devreye giren bir yedek sağlamaktır. Bu yedek iki biçimde yazılabilir: doğrudan bir değer olarak (unwrap_or) ya da bir kapanış (closure) döndüren bir çağrı olarak (unwrap_or_else). İkisi de aynı işi görüyormuş gibi okunur, ama argümanlarının ne zaman değerlendirildiği farklıdır.

Rust’ın Tasarım Hedefleri dersi (M10/K01) bir ifadenin değerlendirilip bir değer ürettiğini kurmuştu; bir işlev çağrısının argümanı da kendi başına bir ifadedir ve çağrı gerçekleşmeden önce değerlendirilir. unwrap_or(hazir_varsayilan(&defter)) yazımı bu kuraldan muaf değildir: hazir_varsayilan(&defter) bir argüman ifadesidir ve unwrap_orun kendisi çalışmadan önce hesaplanır — unwrap_orun içinde Some mu None mi olduğuna bakan bir dallanma varmış gibi görünse de, argüman bu dallanmadan önce çoktan değerlendirilmiştir. unwrap_or_else ise argüman olarak bir değer değil, bir kapanış alır; kapanışın kendisi kurulurken çalışmaz, yalnız unwrap_or_elsein kendi iç uygulaması None dalına girdiğinde çağrılır.

Aşağıdaki koşumda dolu ve bos elle kuruluyor, ama ikisi de bir aramanın döndürebileceği iki durumu temsil ediyor: dolu, anahtarı bulunan bir aramayı; bos, bulunamayan bir aramayı. Ölçülen gövde, hazir_varsayilan işlevi ve iki kapanış — üçü de sayaç defterine bir kayıt düşerek kendi çalıştığını bildiriyor. Aşağıdaki koşum bu farkı doğrudan gösteriyor.

// yoklukdefteri.rs — govde sayma cekirdegi: kere.rs'in Defter/say/kere kesiti
pub type Defter = std::rc::Rc<std::cell::RefCell<std::collections::BTreeMap<String, u32>>>;

pub fn say(defter: &Defter, ad: &str) {
    *defter.borrow_mut().entry(ad.to_string()).or_insert(0) += 1;
}

pub fn kere(defter: &Defter, ad: &str) -> u32 {
    *defter.borrow().get(ad).unwrap_or(&0)
}
// yoklukolcumu.rs — hazir ve tembel varsayilanin kac kez costugu
mod yoklukdefteri;
use yoklukdefteri::{Defter, say, kere};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

fn hazir_varsayilan(defter: &Defter) -> i32 {
    say(defter, "hazir-varsayilan");
    -1
}

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));

    let dolu: Option<i32> = Some(7);
    let bos: Option<i32> = None;

    let a = dolu.unwrap_or(hazir_varsayilan(&defter));
    let b = dolu.unwrap_or_else(|| {
        say(&defter, "tembel-varsayilan-gereksiz");
        -1
    });
    let c = bos.unwrap_or_else(|| {
        say(&defter, "tembel-varsayilan-gerekli");
        -1
    });

    println!("dolu.unwrap_or(...) sonucu: {}", a);
    println!("dolu.unwrap_or_else(...) sonucu: {}", b);
    println!("bos.unwrap_or_else(...) sonucu: {}", c);
    println!();
    for ad in ["hazir-varsayilan", "tembel-varsayilan-gereksiz", "tembel-varsayilan-gerekli"] {
        println!("{:<28}{} kez", ad, kere(&defter, ad));
    }
}
dolu.unwrap_or(...) sonucu: 7
dolu.unwrap_or_else(...) sonucu: 7
bos.unwrap_or_else(...) sonucu: -1

hazir-varsayilan            1 kez
tembel-varsayilan-gereksiz  0 kez
tembel-varsayilan-gerekli   1 kez

dolu doluyken bile hazir_varsayilan bir kez çalışıyor: unwrap_orun argümanı, çağrı gerçekleşmeden önce sıradan bir işlev çağrısı olarak değerlendiriliyor. Optionun Some mu None mi olduğunun burada hiçbir önemi yok — argüman bir ifade olduğu için, kullanılıp kullanılmayacağına bakılmaksızın hesaplanıyor. HT2. Bu yüzden hazir-varsayilan satırı “her seferinde” sınıfına giriyor: unwrap_or her çağrıldığında, argümanı da kaçınılmaz olarak çalışıyor.

unwrap_or_elsee verilen kapanış ise tam tersi davranıyor. dolu üzerinde çağrıldığında gövdesi hiç çalışmıyor — tembel-varsayilan-gereksiz sıfır kalıyor — çünkü kapanış yalnız None durumunda çağrılıyor. bos üzerinde ise aynı kapanış deseni gerçekten çalışıyor ve tembel-varsayilan-gerekli bire çıkıyor. Aradaki fark, argümanın türünde: biri hazır bir değer, öbürü değeri isteğe bağlı olarak üreten bir çağrı. tembel-varsayilan-gerekli “yalnız gerektiğinde” sınıfına giriyor — yokluk gerçekten oluştuğunda, gerektiği kadar çalışıyor.

Arayüzler, Hatalar ve Genellikler kursunun (M09/K02) ölçtüğü şey bir hatanın nerede bağlandığıydı: bildirimde mi, kullanım yerinde mi, değerin içinde mi. Bu ders o soruyu sormuyor — Optionun kendisi zaten M09/K02’nin üçüncü seçeneğiyle aynı aileden, bir değerin içinde taşınan bir durum. Burada ölçülen, o durumu karşılayan gövdenin kaç kez çalıştığı; hazir-varsayilan ile tembel-varsayilan-gerekli aynı sayıyı (bir) verse de, biri koşulsuz çalışıyor, öbürü yalnız gerçekten gerektiğinde.

Yöntem adlarındaki _or ve _or_else ekleri bu ayrımı isimlendiriyor: _or eki, hazır bir değerin doğrudan argüman olarak verildiği yöntemleri işaretliyor; _or_else eki, o değerin yerine bir kapanışın verildiği yöntemleri. Bu adlandırma deseni Optiona özgü değil — standart kütüphane aynı _or / _or_else çiftini başka tiplerde de kullanıyor ve her ikisinde de anlam aynı kalıyor: _or hazır, _or_else tembel. Sıradaki ders bu adlandırma desenini Result üzerinde tekrar karşılayacak.

Option tipinin standart kütüphanedeki yöntemleri bu iki eksende bölünür. unwrap_or gibi hazır bir değer isteyenler her zaman çalışır; unwrap_or_else gibi bir kapanış isteyenler yalnız gerektiğinde çalışır. and_then gibi zincirleme bir yöntem yalnız kapanış tarafında durur: verdiği kapanış yalnız Some durumunda çalışır, None üzerinde hiç çağrılmaz. map_or ise ikisini aynı çağrıda birleştirir — ilk argümanı hazır bir değerdir ve None durumunda bile her seferinde değerlendirilir, ikinci argümanı bir kapanıştır ve yalnız Some durumunda çalışır; tek bir yöntem çağrısının içinde bu dersin ölçtüğü iki sınıf bir arada görülebilir. Yöntem adı _else ekiyle bitmiyor diye bu ayrımın geçerliliği bitmiyor; her yöntemin kendi belgesinde hangi argümanı ne zaman çalıştırdığı, tam olarak bu derste ölçülen eksenle okunabilir.

Sınırlayıcı Ölçüm

Yukarıdaki sayı farkı, tembel yazımın her zaman daha iyi bir seçim olduğu anlamına gelmez. Fark yalnız kaç kez çalıştığındadır; gövde ucuzsa iki yazımın sonucu da birbirinden ayırt edilemez.

// ucuzluk.rs — govde ucuzsa iki yazim da ayni sonucu verir, fark sayida kalir
mod yoklukdefteri;
use yoklukdefteri::{Defter, say, kere};
use std::cell::RefCell;
use std::rc::Rc;
use std::collections::BTreeMap;

fn main() {
    let defter: Defter = Rc::new(RefCell::new(BTreeMap::new()));
    let dolu: Option<i32> = Some(9);

    let a = dolu.unwrap_or({
        say(&defter, "ucuz-hazir");
        0
    });
    let b = dolu.unwrap_or_else(|| {
        say(&defter, "ucuz-tembel");
        0
    });

    println!("hazir yazimin sonucu: {}", a);
    println!("tembel yazimin sonucu: {}", b);
    println!("ucuz-hazir govdesi: {} kez", kere(&defter, "ucuz-hazir"));
    println!("ucuz-tembel govdesi: {} kez", kere(&defter, "ucuz-tembel"));
}
hazir yazimin sonucu: 9
tembel yazimin sonucu: 9
ucuz-hazir govdesi: 1 kez
ucuz-tembel govdesi: 0 kez

HT3. İki satır da aynı sonucu (9) veriyor; program hangi yazımın seçildiğinden etkilenmiyor. Ayrışan tek şey, gövdenin çalışıp çalışmadığı: ucuz-hazir bir kez çalışıyor, ucuz-tembel hiç çalışmıyor. Gövde burada yalnız bir tamsayı döndürdüğü için bu fark hiçbir şeyi değiştirmiyor; ama gövde pahalı bir işse (bir dosya okuma, bir ağ çağrısı, büyük bir hesap), aynı fark artık gözden kaçırılamayacak bir bedele dönüşür. Bu ders o bedeli ölçmüyor — yalnız kaç kez çalıştığını sayıyor; bedelin büyüklüğü kaynakta yazılan gövdenin kendisine bağlı ve bu ders onu değerlendirmiyor.

Bu ölçümün asıl sınırı burada görünüyor. Bir yazılım, bir ayar kaydında bulunmayan bir alanın karşılığını sabit bir tam sayıyla dolduruyorsa, hazır ve tembel yazım arasında gözle görülür bir fark yok — ikisi de aynı sayıyı üretiyor, aradaki tek fark yukarıdaki koşumda görülen sayaç kaydı kadar küçük. Aynı yazılım, bulunmayan bir alanın karşılığını bir dosyadan okuyarak ya da bir toplama hesaplayarak dolduruyorsa, hazır yazım o okumayı ya da hesabı değer zaten varken bile çalıştırır — sonuç yine değişmez, çünkü unwrap_or hesaplanan değeri zaten kullanmayacaktır, ama gövde yine de çalışmış olur. Bu ders hangi gövdenin “ucuz” hangisinin “pahalı” sayılacağına karar vermiyor; yalnız iki yazımın kaç kez çalıştığını sayıp bu kararı kaynak kodu okuyana bırakıyor. Sayı tek başına bir yazımın diğerinden “daha iyi” olduğunu söylemiyor — yalnız neyin çalışıp neyin çalışmadığını görünür kılıyor.

Yukarıdaki iki koşumda “hazır” taraf hep ayrı bir işlev olarak yazıldı, ama bu zorunlu değil. unwrap_or({ say(&defter, "ucuz-hazir"); 0 }) yazımındaki blok da kendi başına bir ifadedir; ayrı bir işlev adı taşımadan da argüman olarak değerlendirilir. Ölçülen sınır bu yüzden bir işlev çağrısına özgü değil — unwrap_orun argümanı ne biçimde yazılırsa yazılsın, bir ifade olduğu sürece çağrılmadan önce değerlendirilir. Aradaki fark yalnız okunabilirliktedir: ayrı bir işlev adı, gövdenin ne yaptığını isimlendirir; gömülü bir blok, aynı işi adsız bırakır.

Bu üç koşum (hazır/tembel karşılaştırması, ucuz gövde karşılaştırması ve aradaki isimlendirme farkı) birlikte tek bir sonuca varıyor: Optiondan değer çıkarmanın hangi yönteminin seçileceği, sonuçtan bağımsız bir sorudur. Sonuç her iki yazımda da aynı; ayrışan yalnız kaynakta yazılan gövdenin çalışıp çalışmadığı ve kaç kez çalıştığı. Bu ayrım küçük bir ayrıntı gibi görünse de, sıradaki dört ders boyunca aynı biçimde tekrar karşılaşılacak: bir tipin hangi yöntemi seçildiğine göre, aynı kaynaktaki bir gövde bazen kaçınılmaz olarak, bazen yalnız gerektiğinde, bazen de hiç çalışmıyor.

Özet

  • Option<T>, bir değerin bulunmayabileceği durumu ayrı bir tip olarak taşır; Some(T) ve None derleme zamanında ayrı ele alınır ve bir match bütünlük ilkesiyle ikisini de kapsamak zorundadır.
  • unwrap_ora verilen argüman bir ifadedir ve değer zaten varken bile her seferinde çalışır — Optionun hangi varyant olduğuna bakmaz.
  • unwrap_or_elsee verilen kapanış yalnız None durumunda çalışır: değer varken hiç çalışmaz, yokluk gerçekleştiğinde yalnız gerektiğinde çalışır.
  • map_or gibi bazı yöntemler bu iki sınıfı aynı çağrıda birleştirir — bir argümanı her seferinde, öbürünü yalnız gerektiğinde çalışır.
  • Gövde ucuzsa iki yazımın sonucu aynıdır; fark yalnız çalışma sayısındadır, gövde pahalılaştıkça bu fark önem kazanır.
  • _or / _or_else adlandırma deseni hazır ve tembel ayrımını standart kütüphane genelinde işaretler; sıradaki derste Result üzerinde de görünür.

Sonraki Adım

Bu ders, bir değerin yok olabileceği durumu ölçtü. Ama bir işlem başarısız olduğunda, çoğu zaman yalnız “yok” demek yetmez — başarısızlığın nedenini de taşımak gerekir. None bir nedeni yoktur, bir işlemin neden başarısız olduğunu söylemez; yalnız bir değerin bulunmadığını söyler. Başarısızlığın Dönüş Değeri dersi aynı “kaç kez koştu” sorusunu, artık bir nedeni de taşıyan bir tipe uyguluyor: başarı ve başarısızlık kolları ayrı gövdeler olarak yazıldığında, aynı işlev arka arkaya birden çok kez çağrıldığında, bir koşumda bu kollardan hangisi çalışıyor ve toplam çağrı sayısı sabit kalırken kol sayıları nasıl ayrışıyor? Bu konunun adı (error-handling) o dersten itibaren gerçek anlamını kazanıyor: artık ölçülen yalnız bir değerin yokluğu değil, adı konmuş bir başarısızlık.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat