İçeriğe geç
academia.sh

Ders 20 / 21

Sonuç Kurucular

Kendi sonuç kurucumuzu yazar; seçilmeyen if ve else dalındaki ifadelerin hiç değerlendirilmediğini, for döngüsünün her elemanı değerlendirdiğini ve seçimin derlemede değil koşumda yapıldığını ölçer.

Yayın Güncelleme

Bu derste

  • Bir dizgi listesi kuran kendi sonuç kurucusunu buildBlock ve buildExpression ile yazar
  • Seçilmeyen if ve else dalındaki ifadelerin hiç değerlendirilmediğini ölçer
  • for döngüsünün buildArray üzerinden her elemanı her çağrıda değerlendirdiğini gösterir
  • Hangi dalın seçileceğinin derlemede değil koşumda belirlendiğini ayırt eder
İçindekiler

Önceki ders bir özelliğin değer kuralını ayrı bir tipe taşıdı. Sonuç kurucu (@resultBuilder) aynı fikri bir ifade listesine uygular. Bir kapanışın gövdesinde alt alta yazılan ifadeler normalde tek tek çalışır ve değerleri atılır. Sonuç kurucu ile işaretlenmiş bir parametreye verilen kapanışta ise her satır kurucu tipin bir yöntemine verilir ve yöntemlerin döndürdüğü değerler birleştirilerek tek bir sonuç elde edilir. Hangi satırın hangi anda çalışacağını, bu yöntemlerden hangisine nasıl bağlandığı belirler.

Bu ders bir dizgi listesi kuran küçük bir kurucu yazar ve üç şeyi ölçer: seçilmeyen if dalındaki ifadenin değerlendirilip değerlendirilmediğini, for döngüsünün elemanları nasıl işlediğini ve dalın seçiminin ne zaman yapıldığını. Dönüşümün kendisi derleme sırasında olur ve koşumda görülmez; ölçülen şey, dönüşmüş kodun koşum sırasındaki davranışıdır. Derleme ile koşum arasındaki bu ayrım önceki bir müfredatta ayrıca ele alınmıştı.

// 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)")
    }
}
// ListBuilder.swift — dizgi listesi kuran sonuc kurucu
@resultBuilder
struct ListBuilder {
    static func buildExpression(_ line: String) -> [String] {
        [line]
    }

    static func buildBlock(_ parts: [String]...) -> [String] {
        parts.flatMap { $0 }
    }

    static func buildOptional(_ part: [String]?) -> [String] {
        part ?? []
    }

    static func buildEither(first part: [String]) -> [String] {
        part
    }

    static func buildEither(second part: [String]) -> [String] {
        part
    }

    static func buildArray(_ parts: [[String]]) -> [String] {
        parts.flatMap { $0 }
    }
}

func makeList(@ListBuilder _ content: () -> [String]) -> [String] {
    content()
}

Kurucuyu Yazmak

ListBuilder altı statik yöntem taşır. buildExpression her tek ifadeyi bir dizgi listesine çevirir. buildBlock bir bloktaki parçaları art arda birleştirir. buildOptional, else dalı olmayan bir if’in sonucunu karşılar; dal seçilmediyse boş liste verir. buildEither(first:) ve buildEither(second:), if ve else dallarından hangisi seçildiyse onu karşılar. buildArray bir for döngüsünün her yinelemesinden gelen parçaları birleştirir. makeList kapanışı ListBuilder ile işaretlenmiş bir parametre olarak alan yardımcı işlevdir.

Seçilmeyen Dal Değerlendirilmez

İlk ölçüm if/else içinde iki ifade yazar. Biri chosen adlı Probe’u, öteki skipped adlı Probe’u çağırır. Koşul her zaman doğrudur, yani yalnız birinci dal seçilir. render işlevi iki kez çağrılır.

// main.swift — secilmeyen dalin ifadesi degerlendirilmez
let chosen = Probe("secilen-dal")
let skipped = Probe("secilmeyen-dal")

do {
    let useFirst = true

    func render() -> [String] {
        makeList {
            "baslik"
            if useFirst {
                chosen.eval("birinci")
            } else {
                skipped.eval("ikinci")
            }
        }
    }

    for _ in 1...2 {
        let lines = chosen.use { skipped.use { render() } }
        print(lines.joined(separator: ","))
    }
}

chosen.report()
skipped.report()
baslik,birinci
baslik,birinci
secilen-dal: 2 degerlendirme, 2 kullanim -> her-kullanimda
secilmeyen-dal: 0 degerlendirme, 2 kullanim -> hic

İki çağrıda da liste baslik,birinci olur. secilen-dal iki değerlendirme kaydeder, çünkü kurucunun gövdesi her render çağrısında yeniden çalışır. secilmeyen-dal ise iki kullanım görür ama sıfır değerlendirme: ikinci dalın ifadesi hiçbir zaman çalışmaz. Kural hic der. Bu dersin anı hiçtir: seçilmeyen dalın ifadesi hiçbir anda değerlendirilmez. İfadenin yalnızca yazılmış olması, onu çağırmaz.

Döngü Her Elemanı Değerlendirir

for döngüsü buildArray ile birleştirilir ve gövdesindeki ifade her eleman için bir kez değerlendirilir. Ölçüm üç elemanlı bir diziyi iki kez işler ve aynı kapanışa if içeren bir satır da ekler; bu satır buildOptional yoluna girer.

// main.swift — for her elemani degerlendirir
let items = Probe("dongu-elemani")

do {
    func render(names: [String]) -> [String] {
        makeList {
            for name in names {
                items.eval(name)
            }
            if names.count > 2 {
                "uzun liste"
            }
        }
    }

    for _ in 1...2 {
        let lines = items.use { render(names: ["a", "b", "c"]) }
        print(lines.joined(separator: ","))
    }
}

items.report()
a,b,c,uzun liste
a,b,c,uzun liste
dongu-elemani: 6 degerlendirme, 2 kullanim -> her-kullanimda

Her çağrıda liste a,b,c,uzun liste olur. dongu-elemani altı değerlendirme kaydeder: iki çağrıda üçer eleman. Elemanlar bir kez önbelleğe alınmadı; kurucunun gövdesi her çağrıda baştan koşar ve döngü her elemanı baştan işler. Anı her-kullanimdadır. if names.count > 2 satırının dizgisi bir Probe’dan geçmedi; yalnızca listede göründü.

İfade Dönüşümü

buildExpression, bloktaki her tek ifadeyi kurucunun parça tipine çevirir. Aynı ada sahip birden çok buildExpression yazılırsa, derleyici ifadenin tipine uyanı seçer. Aşağıdaki MixedBuilder hem String hem Int kabul eder; sayıları sayi 42 biçiminde bir dizgiye çevirir. Ölçüm iki ifadeyi ayrı Probe’lardan geçirir ve her çağrıda her ifadenin kaç kez değerlendirildiğine bakar.

// MixedBuilder.swift — metin ve sayi ifadesi kabul eden sonuc kurucu
@resultBuilder
struct MixedBuilder {
    static func buildExpression(_ line: String) -> [String] {
        [line]
    }

    static func buildExpression(_ number: Int) -> [String] {
        ["sayi \(number)"]
    }

    static func buildBlock(_ parts: [String]...) -> [String] {
        parts.flatMap { $0 }
    }
}

func makeMixed(@MixedBuilder _ content: () -> [String]) -> [String] {
    content()
}
// main.swift — buildExpression ifadeyi cevirir, yeniden degerlendirmez
let texts = Probe("metin-ifadesi")
let numbers = Probe("sayi-ifadesi")
do {
    for _ in 1...2 {
        let lines = texts.use {
            numbers.use {
                makeMixed {
                    texts.eval("baslik")
                    numbers.eval(42)
                }
            }
        }
        print(lines.joined(separator: ","))
    }
}
texts.report()
numbers.report()
baslik,sayi 42
baslik,sayi 42
metin-ifadesi: 2 degerlendirme, 2 kullanim -> her-kullanimda
sayi-ifadesi: 2 degerlendirme, 2 kullanim -> her-kullanimda

Çıktı iki çağrıda da baslik,sayi 42 verir: dizgi ifadesi olduğu gibi geçti, tam sayı ifadesi sayi 42 biçimine çevrildi. Çevirinin hangi yoldan yapıldığı ifadenin tipinden anlaşılır; kurucuya hiçbir ayırma yazılmadı. Sayaçlar dönüşümün değerlendirmeyi değiştirmediğini gösterir. metin-ifadesi ve sayi-ifadesi iki çağrıda ikişer değerlendirme kaydeder ve her-kullanimda der. buildExpression ifadenin sonucunu alır, ifadeyi yeniden çalıştırmaz. Kurucu kapanışın gövdesini çağrı başına bir kez baştan koşturur ve her ifade tam bir kez değerlendirilir. Bu yüzden dönüşüm yazarken yan etkili bir ifadenin tekrarlanacağından endişe edilmez. Aynı ad altında birden çok buildExpression yazmak, kurucunun kabul ettiği ifade tiplerini genişletir; tipi karşılamayan bir ifade ise derleme hatası verir, çalışma zamanında sessizce atlanmaz.

İç İçe Koşul

if içindeki if, kurucuda iç içe iki buildOptional yolu açar. Dış koşul tutmazsa iç koşulun kendisi de değerlendirilmez. Ölçüm aynı kapanışı iki kez visible: false ile, sonra iki kez visible: true ile çalıştırır. İç koşul ve iç satırın ifadesi için her durumda ayrı birer Probe vardır.

// main.swift — dis kosul tutmazsa ic kosul degerlendirilmez
let closedGate = Probe("kapali-ic-kosul")
let closedHit = Probe("kapali-ic-satir")
let openGate = Probe("acik-ic-kosul")
let openHit = Probe("acik-ic-satir")
do {
    func render(visible: Bool, gate: Probe, hit: Probe) -> [String] {
        makeList {
            if visible {
                if gate.eval(true) {
                    hit.eval("ic")
                }
            }
        }
    }

    for _ in 1...2 {
        let shut = closedHit.use { closedGate.use { render(visible: false, gate: closedGate, hit: closedHit) } }
        let shown = openHit.use { openGate.use { render(visible: true, gate: openGate, hit: openHit) } }
        print("kapali \(shut.count), acik \(shown.count)")
    }
}
for probe in [closedGate, closedHit, openGate, openHit] {
    probe.report()
}
kapali 0, acik 1
kapali 0, acik 1
kapali-ic-kosul: 0 degerlendirme, 2 kullanim -> hic
kapali-ic-satir: 0 degerlendirme, 2 kullanim -> hic
acik-ic-kosul: 2 degerlendirme, 2 kullanim -> her-kullanimda
acik-ic-satir: 2 degerlendirme, 2 kullanim -> her-kullanimda

Dış koşul false iken ne iç koşul ne de iç satır değerlendirilir. kapali-ic-kosul ve kapali-ic-satir ikişer kullanıma karşılık sıfır değerlendirme kaydeder ve hic der; çıktıda kapali 0 satırı da listenin boş kaldığını gösterir. Dış koşul true iken ise iki iç ifade de her çağrıda bir kez değerlendirilir: acik-ic-kosul ve acik-ic-satir ikişer değerlendirme kaydeder ve her-kullanimda der. acik 1 satırı iç satırın listeye girdiğini doğrular. Koşulların sırası burada da kısa devre gibi davranır: sıradaki ifade yalnız öncekinin yolu açıksa değerlendirilir. Fark, bunun && operatöründen değil, kurucunun buildOptional yolundan gelmesidir. Her if kendi buildOptional çağrısını üretir; dıştaki çağrının parçası boşsa içtekine hiç inilmez ve içteki ifade bu yüzden hic kalır.

Boş Döngü

buildArray, döngünün her yinelemesinden gelen parçaları alır. Döngü hiç dönmezse parça listesi boştur ve buildArray boş bir liste üretir. Soru, boş bir dizi üzerinde kurulan döngünün gövdesindeki ifadenin değerlendirilip değerlendirilmediğidir. Ölçüm aynı kapanışı bir kez boş, bir kez dolu bir dizi ile çalıştırır.

// main.swift — bos dizi uzerinde dongu govdesi degerlendirilmez
let emptyLoop = Probe("bos-dongu")
let filledLoop = Probe("dolu-dongu")
do {
    func render(names: [String], probe: Probe) -> [String] {
        makeList {
            for name in names {
                probe.eval(name)
            }
        }
    }

    for _ in 1...2 {
        let none = emptyLoop.use { render(names: [], probe: emptyLoop) }
        let some = filledLoop.use { render(names: ["x", "y"], probe: filledLoop) }
        print("bos \(none.count), dolu \(some.count)")
    }
}
emptyLoop.report()
filledLoop.report()
bos 0, dolu 2
bos 0, dolu 2
bos-dongu: 0 degerlendirme, 2 kullanim -> hic
dolu-dongu: 4 degerlendirme, 2 kullanim -> her-kullanimda

Boş dizi üzerindeki döngüde gövdenin ifadesi hiç çalışmaz: bos-dongu iki kullanıma karşılık sıfır değerlendirme kaydeder ve hic der. bos 0 satırı sonucun da boş liste olduğunu söyler; buildArray boş parça listesini boş bir sonuca çevirir ve hata vermez. Dolu dizide her çağrı iki eleman işler, dolayısıyla dolu-dongu iki çağrıda dört değerlendirme kaydeder ve her-kullanimda der. Değerlendirme sayısı döngünün dönüş sayısına eşittir. Kurucu gövdesinde döngü kuran biri için bu bir ölçüdür: elemanları yan etkili bir ifadeyle işleyen döngü, her çağrıda eleman sayısı kadar koşar. Koleksiyon çağrılar arasında saklanmaz; yan etkinin tekrarı gerekmiyorsa döngü kurucunun dışına alınır ve sonuç kurucuya hazır verilir.

Bu ölçümlerin ortak dersi şudur: kurucu, gövdedeki her satırı bir çağrıya çevirir ve hangi çağrının yapılacağını yazım biçimi belirler. Bir ifadenin değerlendirilip değerlendirilmeyeceğini okuyarak tahmin etmek yerine, dönüşümü zihinde açmak gerekir: ifade bir buildExpression çağrısına, koşul bir buildOptional (else varsa buildEither) çağrısına, döngü bir buildArray çağrısına dönüşür. Yan etkisi olan bir ifadeyi gövdeye koymadan önce bu çağrı zincirinde kaç kez ve hangi koşulda çalışacağı sorulur.

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

Dönüşüm derlemede yapılır, ama hangi dalın seçileceği koşumda belirlenir. Önceki iki ölçümde koşul sabitti ve bu fark görünmüyordu. Son ölçüm koşulu bir parametre yapar ve render’ı sırayla true, false, true ile çağırır. Her iki dalın ifadesi ayrı birer Probe’a yazılır.

// main.swift — dal secimi kosumda yapilir
let left = Probe("sol-dal")
let right = Probe("sag-dal")

do {
    func render(first: Bool) -> [String] {
        makeList {
            if first {
                left.eval("sol")
            } else {
                right.eval("sag")
            }
        }
    }

    for flag in [true, false, true] {
        let lines = left.use { right.use { render(first: flag) } }
        print(lines.joined(separator: ","))
    }
}

left.report()
right.report()
sol
sag
sol
sol-dal: 2 degerlendirme, 3 kullanim -> her-kullanimda
sag-dal: 1 degerlendirme, 3 kullanim -> ilk-kullanimda

Çağrılar sırasıyla sol, sag, sol yazar. sol-dal iki değerlendirme kaydeder: yalnız true ile gelen iki çağrıda koşar. sag-dal ise tek değerlendirme kaydeder ve bu değerlendirme ikinci çağrıda, yani ilk kullanımdan sonra olduğu için kural ilk-kullanimda der. İfade, dalın seçildiği çağrıda değerlendirilir; kodda yazılı olmasının tek başına bir anlamı yoktur. Bu yüzden hic anı yalnızca dal hiç seçilmediğinde geçerlidir ve koşula bağlıdır.

BL5. Ölçülen kurucu yalnız dizgi listesi kurar ve buildLimitedAvailability gibi ek yöntemler tanımlanmadı. İki dallı if, üç dallı if/else if zincirlerinde aynı kuralın geçtiği ölçülmedi. Tek bir kurucu tipinin kullanılması tasarım tercihidir.

Özet

  • Sonuç kurucu, kapanış gövdesindeki ifadeleri buildBlock, buildExpression, buildOptional, buildEither ve buildArray yöntemleriyle bir sonuca birleştirir.
  • if/else içinde seçilmeyen dalın ifadesi hiç değerlendirilmez; yazılmış olması onu çağırmaz.
  • Kurucunun gövdesi her çağrıda baştan koşar ve for döngüsü her elemanı her çağrıda değerlendirir.
  • Dönüşüm derlemede yapılır, ama hangi dalın seçileceği koşumda belirlenir; hic anı yalnız dal hiç seçilmezse geçerlidir.
  • Bu dersin anı: hiç. Seçilmeyen dalın ifadesi hiçbir anda değerlendirilmez.

Sonraki Adım

Bu konunun ilk dersleri tek bir akışta çalışan koddaki bellek ve değerlendirme anlarını ölçtü. Birden çok iş aynı anda aynı belleğe dokunduğunda yeni sorular çıkar: bir değer kimin kontrolünde, kim ona ne zaman erişir. Sonraki ders aktörü, aktarılabilir tip anlamına gelen Sendable’ı ve @MainActor’ı anlatır; aktörün tembel özelliğinin ilk erişimde nasıl davrandığını ve async let ile başlatılan işi ölçer.

İ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