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)veNonederleme zamanında ayrı ele alınır ve birmatchbü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ızNonedurumunda çalışır: değer varken hiç çalışmaz, yokluk gerçekleştiğinde yalnız gerektiğinde çalışır.map_orgibi 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_elseadlandırma deseni hazır ve tembel ayrımını standart kütüphane genelinde işaretler; sıradaki dersteResultü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.