İçeriğe geç
academia.sh

Ders 09 / 21

İsteğe Bağlı Değerler

Yokluğun tipte ifade edilmesinin, nil üzerinde dönüştürücü kapanışın hiç çalışmamasının ve iç içe katmanlarda yokluğun nerede durduğunun ölçümü.

Yayın Güncelleme

Bu derste

  • İsteğe bağlı değerin bir numaralandırma olduğunu ve değerle aynı tip olmadığını gösterir
  • nil üzerinde map çağrısında dönüştürücü kapanışın hiç değerlendirilmediğini ölçer
  • map ile flatMap arasındaki katman farkını çıktıdan okur
  • İç içe katmanlarda dış katmanın dolu olmasının dönüştürücüyü çalıştırdığını ölçer
İçindekiler

Önceki ders kapanışları ve yakalama listesini ölçtü. Bir kapanışın gövdesi yazıldığı yerde değil, çağrıldığı yerde koşar; hiç çağrılmayan bir kapanışın gövdesi hiç değerlendirilmez. Bu ders aynı soruyu, bir kapanışı parametre olarak alan ve bazen o kapanışı hiç çağırmayan bir yöntem üzerinden sorar. O yöntem, tip sisteminin en çok kullanılan parçasına aittir: yokluğu bir değer gibi taşıyan tipe.

Çoğu dil yokluğu özel bir değerle, örneğin bir boş gösterici ya da sihirli bir sayıyla ifade eder ve o değerin her yerde kontrol edilmesini programcıya bırakır. Swift yokluğu tipin kendisine yazar. İsteğe bağlı değer (optional), ya bir değer taşıyan ya da hiçbir şey taşımayan bir kutudur ve Int? biçiminde yazılır. Int her zaman bir tamsayıdır; Int? ise tamsayı olabilir, olmayabilir. Derleyici bu iki tipi birbirinin yerine kullandırmaz, dolayısıyla yokluğun kontrolü kendiliğinden unutulamaz; onu atlamak için zorla açmayı bilerek yazmak gerekir.

Kursun ölçü ekseni bu derste de değişmez: bu ifade ne zaman değerlendiriliyor? Dört an vardır: hemen, ilk kullanımda, her kullanımda, hiç. Kuralı sayan Probe sınıfı aşağıda olduğu gibi verilir. Bu ders eksenin dördüncü anına, yani hiç değerlendirilmeyen ifadeye odaklanır.

Çekirdek

Her ders aynı çekirdeği kullanır. Probe bir ifadenin kaç kez değerlendirildiğini (eval) ve kaç kez kullanıldığını (use) sayar; moment özelliği bu iki sayıdan anı çıkarır. Açılış dersindeki bağlayıcı ölçümde sekiz ifadenin dağılımı 2/1/3/2 idi: ikisi hemen, biri ilk kullanımda, üçü her kullanımda, ikisi hiç. Bu derste yalnız kendi kesitini ölçeriz.

// Probe.swift — bir ifadenin ne zaman degerlendirildigini kaydeder
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "hic" }
        if evaluations > 1 { return "her-kullanimda" }
        return evaluatedBeforeUse ? "hemen" : "ilk-kullanimda"
    }

    func report() {
        print("\(label): \(evaluations) degerlendirme, \(uses) kullanim -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["hemen", "ilk-kullanimda", "her-kullanimda", "hic"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}

Yokluk Bir Tip Olarak

Bir isteğe bağlı değerin içinde ne olduğuna bakmadan önce kendisine bakalım. Int("42") bir metni tamsayıya çevirir ve çevirme başarısız olabileceği için dönüş tipi Int? olur. Başarılı olursa kutu doludur, Int("x") gibi bir çağrıda kutu boştur ve değeri nil’dir.

// main.swift — istege bagli deger bir numaralandirmadir
func describe(_ value: Int?) -> String {
    switch value {
    case .some(let number):
        return "dolu \(number)"
    case .none:
        return "bos"
    }
}

let parsed = Int("42")
let broken = Int("x")
print(type(of: parsed))
print(describe(parsed))
print(describe(broken))
print(parsed == .some(42), broken == .none)
print(broken == nil)
Optional<Int>
dolu 42
bos
true true
true

İlk satır tipin adını verir: Int? yalnızca bir kısaltmadır, gerçek tip Optional<Int>’tir. Optional standart kitaplıkta tanımlı bir numaralandırmadır (enumeration) ve iki durumu vardır: bir değer taşıyan .some ve hiçbir şey taşımayan .none. Bu yüzden describe işlevi kutuyu bir switch ile açabilir; her durum için ayrı bir kol yazılır ve .some kolunda taşınan değer number adına bağlanır. Dördüncü satır 42 değerinin .some(42) ile, boş kutunun .none ile eşit olduğunu gösterir; beşinci satır nil yazımının .none için kısaltma olduğunu gösterir.

Aynı çıktıdan ikinci bir sonuç çıkar: Int? ile Int iki ayrı tiptir. Aşağıdaki parçalar derlenmez; her ikisi de önce geçici bir ölçülen blokta koşturuldu ve derleyici ikisini de reddetti. Yazımları burada yalnızca anlatım için durur.

let a: Int? = 5
let b = a + 1   // derlenmez: Int? ile Int toplanamaz
let c: Int = nil   // derlenmez: nil yalnizca istege bagli bir tipe atanabilir

Birinci satırdaki toplama, kutunun içindeki sayıyı değil kutunun kendisini görür ve derleyici kutunun açılmasını ister. İkinci satırdaki atama ise Int tipine yokluk yazmaya çalışır. Yokluğu taşıyabilecek tek yer, tipi ? ile işaretlenmiş olan yerdir. Kutunun nasıl açılacağı bir sonraki dersin konusudur; bu derste kutuyu açmadan onunla çalışan iki yöntemi ölçeceğiz.

Yokluğun Kaynakları

İsteğe bağlı değerler programa yalnızca metin çevirmeyle girmez. Standart kitaplık, yokluğun olağan olduğu birçok yerde bir isteğe bağlı değer döndürür; ama her yerde değil: sınır dışındaki bir dizi indeksi nil vermez, programı durdurur. Bir sözlükte olmayan anahtarın değeri yoktur; boş bir dizinin ilk elemanı yoktur. Bu durumlarda özel bir hata değeri ya da bir istisna beklemek gerekmez: dönüş tipi zaten “belki” der.

// main.swift — standart kitaplik yoklugu istege bagli degerle bildirir
let ages = ["ada": 36, "alan": 41]
let names: [String] = []
print(ages["ada"], ages["grace"])
print(names.first, [3, 1, 2].first)
print(ages["ada"] == 36, ages["grace"] == nil)
Optional(36) nil
nil Optional(3)
true true

Birinci satırda var olan anahtarın değeri Optional(36) olarak gelir; yani 36 bile doğrudan bir Int değil, içinde 36 bulunan bir kutudur. Olmayan anahtar nil verir. İkinci satır aynı örüntüyü diziler için gösterir: boş dizinin first özelliği nil, dolu dizinin first özelliği kutulanmış bir 3 döndürür. Üçüncü satırda kutu, kutulanmamış bir sayıyla karşılaştırılır: == işleci, iki tarafın da Int? olmasını sağlayacak biçimde sağdaki 36’yı otomatik olarak kutular. Bu yüzden ages["ada"] == 36 derlenir; yukarıdaki + örneği ise kutunun içindeki değeri gerektirdiği için derlenmez.

Bu kaynaklar bir ayrımı görünür kılar: yokluğun nedeni isteğe bağlı değerin tipinde yazılı değildir. ages["grace"] boştur çünkü anahtar yoktur, Int("x") boştur çünkü metin sayı değildir, ama ikisi de aynı nil’dir. Neden bilgisine ihtiyaç duyulduğunda hata yönetimi devreye girer; o konu bu kursta daha sonra gelir. Yokluğun nedeni önemli olmadığında isteğe bağlı değer, en az yük getiren araçtır.

nil Üzerinde map

Optional iki durumlu bir kutu olduğuna göre, kutunun içindeki değeri dönüştüren bir yöntem de iki durumda farklı davranır. map bir dönüştürücü kapanış (closure) alır. Kutu doluysa kapanış içindeki değerle çağrılır ve sonuç ayrı bir kutuya konur; kutu boşsa kapanış çağrılmaz ve sonuç boş bir kutudur. Bunu ölçelim: iki kutu, biri dolu biri boş, her biri iki kez kullanılır ve dönüştürücü kapanışın gövdesi eval ile işaretlenir.

// main.swift — nil uzerinde map donusturucuyu cagirmaz
let filled = Probe("map-dolu")
let vacant = Probe("map-bos")
let present: Int? = Int("21")
let missing: Int? = Int("yirmi bir")

for _ in 1...2 {
    _ = filled.use { present.map { filled.eval($0 * 2) } }
    _ = vacant.use { missing.map { vacant.eval($0 * 2) } }
}
filled.report()
vacant.report()
map-dolu: 2 degerlendirme, 2 kullanim -> her-kullanimda
map-bos: 0 degerlendirme, 2 kullanim -> hic

Dolu kutuda map her çağrıldığında dönüştürücüyü yeniden koşturur: iki kullanım, iki değerlendirme, an her kullanımda. Sonucun bir yerde saklanmadığına dikkat edin; use içindeki ifade her seferinde baştan çalışır. Boş kutuda iki kullanım vardır ama değerlendirme yoktur, çünkü map kutunun boş olduğunu görür görmez dönüştürücüye uğramadan döner. Gövdede yazılı filled.eval($0 * 2) ya da vacant.eval($0 * 2) ifadesi kaynakta durur, fakat boş kutuda hiçbir zaman değerlendirilmez. Kural bu durumu hiç diye adlandırır.

Bu ölçüm, kapanışın gövdesinin ne zaman koştuğunu bir kez daha gösterir: kapanış yazıldığı yerde koşmaz, ve onu çağırması beklenen yöntem de onu her zaman çağırmaz. Hiç çağrılmayan bir dönüştürücünün içindeki bir hata, bir yan etki ya da pahalı bir hesap hiçbir zaman gerçekleşmez. Dönüştürücünün yan etkisine güvenen bir kod, boş kutuda o yan etkiyi kaçırır.

TS1. Bu ölçüm yalnız map için; filter, compactMap ya da forEach gibi başka yöntemlerin boş kutudaki davranışı denenmedi. Dönüştürücünün yan etkiyle değil saf bir hesapla yazıldığı durumlar da ayrıca ölçülmedi.

Zincirde İki map

Boş kutu bir map çağrısından boş çıkıyorsa, ardına eklenen ikinci map de aynı kutuyla karşılaşır. Zincirin ilk halkası hiçbir şey üretmediğinde sonraki halkalar üretecek bir değer bulamaz. Bunu iki dönüştürücüyle ölçelim; her dönüştürücünün gövdesi kendi Probe’una bağlıdır ve zincir her kullanımda baştan kurulur.

// main.swift — bos kutuda zincirdeki her map atlanir
let stageOne = Probe("zincir-birinci")
let stageTwo = Probe("zincir-ikinci")
let seed: Int? = Int("on")

for _ in 1...2 {
    _ = stageOne.use { seed.map { stageOne.eval($0 + 1) }.map { stageTwo.eval($0 * 2) } }
    _ = stageTwo.use { seed.map { stageOne.eval($0 + 1) }.map { stageTwo.eval($0 * 2) } }
}
stageOne.report()
stageTwo.report()
zincir-birinci: 0 degerlendirme, 2 kullanim -> hic
zincir-ikinci: 0 degerlendirme, 2 kullanim -> hic

İki dönüştürücü de hiç değerlendirilmedi. İkincinin hiç çalışmaması beklenebilirdi, çünkü kendi girdisi birincinin sonucudur; birincinin hiç çalışmaması ise kutunun en başta boş olmasından gelir. seed değeri Int("on") ile kurulduğu için nil’dir; ilk map boş kutuyu dönüştürmeden geçirir, ikinci map de aynı boş kutuyu alır. Uzun bir dönüştürücü zincirinde en baştaki bir yokluk bu yüzden sonraki halkaların hiçbirini koşturmaz.

TS2. Bu zincirde yalnız başlangıç değeri boştur; zincirin ortasındaki bir dönüştürücünün nil döndürdüğü durum bu ölçümde yok ve orada sonraki mapin davranışı ayrıca ölçülmedi.

map ile flatMap

Dönüştürücünün kendisi de isteğe bağlı bir değer döndürebilir. Bir metni tamsayıya çeviren Int($0) tam böyle bir ifadedir: sonucu Int?’tir. map dönüştürücünün sonucunu olduğu gibi ayrı bir kutuya koyar, bu yüzden kutunun içine bir kutu girer. flatMap ise dönüştürücünün döndürdüğü kutuyu dışarıya düzleştirir. Üç girdiyle farkı görelim: bir geçerli metin, bir geçersiz metin ve bir boş kutu.

// main.swift — map ic ice katman uretir, flatMap duzlestirir
let inputs: [String?] = ["7", "x", nil]
for input in inputs {
    let nested = input.map { Int($0) }
    let flat = input.flatMap { Int($0) }
    print(nested, flat)
}
Optional(Optional(7)) Optional(7)
Optional(nil) nil
nil nil

Birinci satırda map iki katmanlı bir sonuç verir, flatMap tek katmanlı. İkinci satır asıl farkı gösterir: "x" geçersiz bir metindir ve map sonucu Optional(nil) olur, yani dış kutu dolu, iç kutu boştur; flatMap ise doğrudan nil verir. Üçüncü satırda girdinin kendisi boş olduğu için iki yöntem de dönüştürücüye hiç uğramaz ve nil döner.

İkinci satır sonraki ölçümün zeminidir. Optional(nil) ile nil birbirinden ayrı değerlerdir ve ikisi de yokluk anlatır; fakat biri yokluğu bir katman içeride, öbürü en dışta taşır. Hangi katmanda durduğu, bir sonraki yöntemin dönüştürücüyü çağırıp çağırmayacağını belirler.

TS3. Bu çıktı yalnız Int dönüştürmesi için; dönüştürücünün başka bir tipe çevirdiği ya da hata fırlattığı durumlar denenmedi. Çıktıdaki Optional(...) yazımı bu sürümün print biçimidir; ölçülen şey katman sayısıdır, biçimin kendisi değil.

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

Ders başında map için bir kural kurduk: boş kutuda dönüştürücü çalışmaz. Kural doğru, ama hangi kutunun boş sayıldığı bir katman sorusudur. Int?? tipi iki katmanlı bir kutudur. Dış kutu doluyken içindeki kutu boş olabilir; bu durum .some(nil) diye yazılır. Aşağıda iki değer var: dış katmanı dolu olan ama içi boş bir değer ve en dışta boş olan bir değer. Her ikisine de map uygulanır ve dönüştürücü, aldığı değerin nil olup olmadığını döndürür.

// main.swift — dis katman doluysa yokluk iceride olsa da map kosar
let outer = Probe("dis-katman-dolu")
let hollow = Probe("dis-katman-bos")
let inner: Int?? = .some(nil)
let empty: Int?? = nil

for _ in 1...2 {
    _ = outer.use { inner.map { value in outer.eval(value == nil) } }
    _ = hollow.use { empty.map { value in hollow.eval(value == nil) } }
}
outer.report()
hollow.report()
print(inner.map { $0 == nil })
print(empty.map { $0 == nil })
dis-katman-dolu: 2 degerlendirme, 2 kullanim -> her-kullanimda
dis-katman-bos: 0 degerlendirme, 2 kullanim -> hic
Optional(true)
nil

Dış katmanı dolu olan değerde dönüştürücü çalıştı ve nil aldı: sonuç Optional(true)’dur, çünkü dönüştürücü içerideki yokluğu true olarak döndürdü. En dışta boş olan değerde dönüştürücü hiç çalışmadı. Yani map için yokluk, yalnızca en dış kutunun boş olmasıdır; içeride yokluk taşıyan ama dışı dolu bir değer, map açısından doludur.

Bu sonuç, dersin kuralının geçerli olduğu alanı daraltır. Dönüştürücü kapanışın hiç değerlendirilmemesi, kutunun dış katmanının boş olmasına bağlıdır. İç içe isteğe bağlı değerlerle çalışırken map yokluğu “bir katman yukarı” taşımaz: iç katmandaki yokluğu dönüştürücüye değer olarak verir. Bu yüzden önceki bölümdeki Optional(nil) sonucu, sonraki bir map çağrısı için dolu bir kutudur. Katman sayısını azaltmak istendiğinde flatMap kullanılır.

TS4. Bu ölçüm iki katmanlı bir kutu için; üç ya da daha çok katmanlı kutular denenmedi. Dönüştürücünün nil aldığını biz value == nil karşılaştırmasıyla gözlemledik; başka bir gözlem biçimi denenmedi.

Özet

  • Int? ile Int ayrı tiplerdir; yokluk tipin içinde taşınır ve derleyici kontrolü zorunlu kılar.
  • Optional bir numaralandırmadır: .some bir değer taşır, .none (yani nil) hiçbir değer taşımaz.
  • Boş kutuda map dönüştürücü kapanışı hiç çağırmaz; dolu kutuda her çağrıda yeniden koşturur.
  • Dönüştürücü isteğe bağlı bir değer döndürdüğünde map katman ekler, flatMap düzleştirir; Optional(nil) ile nil ayrı değerlerdir.
  • Dış katmanı dolu olan Int?? içinde yokluk bulunsa bile map dönüştürücüyü çalıştırır.
  • Bu dersin anı: hiç. Boş bir kutuda map dönüştürücüsü hiç değerlendirilmez.

Sonraki Adım

Bu derste kutuyu açmadan çalıştık: map ve flatMap değeri dönüştürdü ama sonucu yine bir kutuda bıraktı. Gerçek bir değere ihtiyaç duyulduğunda kutunun açılması gerekir. Bir sonraki ders açmanın yollarını ölçer: if let ile guard let sağ tarafı ne zaman değerlendirir, ?? zinciri kaçıncı adayda durur ve isteğe bağlı zincirleme nil gördüğünde zincirin geri kalanına ne yapar.

İ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