İçeriğe geç
academia.sh

Ders 12 / 19

Tepkisel Akışlar

Bir yayımcıya yinelenen abonelikte isteğin tekrarlandığının, paylaşımlı akışta ise sağlam kaldığının, ardışık tekrarı düşüren işlecin ise değer sırasına bağlı olduğunun ölçümü.

Yayın Güncelleme

Bu derste

  • Tepkisel akışta yayımcı, abone ve işleç kavramlarını ayırır
  • Paylaşımsız akışa aynı adla iki kez abone olmanın isteği yinelediğini ölçer
  • Paylaşımlı akışta aboneliğin isteği tekrar etmediğini ve işlemin sağlam kaldığını ölçer
  • Ardışık tekrarı düşüren işlecin sınıfını değer çiftinin belirlediğini gösterir
İçindekiler

Önceki derste kuyruğa ekleme hem yinelemeye hem sıraya duyarlı çıktı, değer atama ise yalnız sıraya. Bu derste eşzamanlı kodun bir üçüncü biçimine bakıyoruz: değerlerin zaman içinde aktığı ve birden çok yerin onları dinlediği tepkisel akışlara.

Çekirdek dosya konunun her dersinde aynıdır.

// Replay.swift — bir islemin yinelemeye ve siraya duyarliligini siniflandirir
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "saglam"
        case (true, false): return "yinelemeye-duyarli"
        case (false, true): return "siraya-duyarli"
        case (true, true): return "cift-duyarli"
        }
    }

    func report() -> String {
        print("\(label) | bir=\(once) iki=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["saglam", "yinelemeye-duyarli", "siraya-duyarli", "cift-duyarli"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}

Bu derste işlemler duruma yalnız bir akışın bilgisini yazar ve eşzamansız değildir; bu yüzden replay ile ölçülür. Model, işlemin duruma ne yaptığını kurar; ağın ya da akışı çalıştıran çerçevenin kendisi değildir.

Yayımcı, Abone ve İşleç

Tepkisel akışta bir yayımcı değer üretir, bir abone o değerleri alır ve aradaki işleçler değerleri dönüştürür ya da eler. Bir map işleci her değeri dönüştürür, bir removeDuplicates işleci ardışık tekrarı düşürür. Yayımcı soğuk olabilir: abone olunduğunda işi başlatır, örneğin ağ isteğini gönderir. Yayımcı paylaşımlı ise iş yalnız ilk abonede başlar ve sonraki aboneler aynı sonucu alır.

Bu ayrım ölçülebilir bir sonuç doğurur. Paylaşımsız bir akışa her abone olunduğunda istek yeniden gider. Paylaşımlı akışta ise ikinci abone isteği tekrarlamaz.

// Feed.swift — akisa abone olma ve ardisik tekrari dusuren islec
struct Delivery: Equatable, CustomStringConvertible {
    var requests: Int
    var listeners: [String]

    var description: String { "istek=\(requests) dinleyen=\(listeners)" }
}

func subscribing(_ name: String) -> (Delivery) -> Delivery {
    { state in
        var next = state
        next.requests += 1
        if !next.listeners.contains(name) { next.listeners = (next.listeners + [name]).sorted() }
        return next
    }
}

func subscribingShared(_ name: String) -> (Delivery) -> Delivery {
    { state in
        var next = state
        if next.listeners.isEmpty { next.requests += 1 }
        if !next.listeners.contains(name) { next.listeners = (next.listeners + [name]).sorted() }
        return next
    }
}

struct Output: Equatable, CustomStringConvertible {
    var emitted: [Int]

    var description: String { "\(emitted)" }
}

func emitting(_ value: Int) -> (Output) -> Output {
    { state in
        state.emitted.last == value ? state : Output(emitted: state.emitted + [value])
    }
}

subscribing paylaşımsız aboneliği kurar: her çağrı isteği bir artırır ve adı dinleyenlere ekler. subscribingShared paylaşımlı olanı kurar: istek yalnız dinleyen yokken artar. Dinleyenler sıralı tutulur, çünkü abone sırası ölçüme girmemeli; bu bir tasarım seçimidir ve ES19’da anılır. emitting ardışık tekrarı düşüren işleci kurar: yeni değer son değere eşitse çıktıya dokunmaz.

Akış Neden Var

Bir değer tek seferde hesaplanıp dönüyorsa eşzamansız işlev yeter. Akış, değerin zaman içinde birden çok kez geldiği durumlar içindir: arama kutusundaki her harf, konum güncellemeleri, bir sunucudan gelen olaylar. Bu durumlarda “değeri al” yerine “değer geldikçe şunu yap” demek gerekir ve akış bu ikinci cümlenin kurulma biçimidir. Yayımcı ile abone arasında bir sözleşme vardır: yayımcı değerleri sırayla verir, abone onları aldığı sırayla işler, aradaki işleçler her değeri geçirir, dönüştürür ya da düşürür.

Bu sözleşme iki şeyi aboneye bırakır. Birincisi aboneliğin ömrüdür: abonelik tutulduğu sürece değer gelir, bırakıldığında akış sonlanır. İkincisi aboneliğin maliyetidir: her abonelik, yayımcı soğuksa, arkasındaki işi yeniden başlatır. Önceki derslerdeki sorular burada aynen sürer; yineleme ve sıra duyarlılığı, değerlerin geliş biçimine taşınmıştır.

Abonelik ve Paylaşım

Önce aynı ad iki kez abone olur. Sonra iki farklı ad, hem paylaşımsız hem paylaşımlı akışa abone olur.

// main.swift — paylasimsiz ve paylasimli abonelik
let fresh = Delivery(requests: 0, listeners: [])

let cold = replay("paylasimsiz-ayni-ad", from: fresh, subscribing("ayse"), then: subscribing("ayse")).report()
let sharedSame = replay("paylasimli-ayni-ad", from: fresh, subscribingShared("ayse"), then: subscribingShared("ayse")).report()
let sharedPair = replay("paylasimli-iki-ad", from: fresh, subscribingShared("ayse"), then: subscribingShared("burak")).report()

countSensitivities([cold, sharedSame, sharedPair])
paylasimsiz-ayni-ad | bir=istek=1 dinleyen=["ayse"] iki=istek=2 dinleyen=["ayse"] ab=istek=2 dinleyen=["ayse"] ba=istek=2 dinleyen=["ayse"] -> yinelemeye-duyarli
paylasimli-ayni-ad | bir=istek=1 dinleyen=["ayse"] iki=istek=1 dinleyen=["ayse"] ab=istek=1 dinleyen=["ayse"] ba=istek=1 dinleyen=["ayse"] -> saglam
paylasimli-iki-ad | bir=istek=1 dinleyen=["ayse"] iki=istek=1 dinleyen=["ayse"] ab=istek=1 dinleyen=["ayse", "burak"] ba=istek=1 dinleyen=["ayse", "burak"] -> saglam
saglam: 2
yinelemeye-duyarli: 1
siraya-duyarli: 0
cift-duyarli: 0

Paylaşımsız akışta ikinci abonelik isteği yeniden gönderir: bir 1, iki 2. Dinleyen listesi aynı kalır, ama istek sayısı artar. İşlem yinelemeye duyarlıdır. Paylaşımlı akışta istek yalnız ilk abonede artar; aynı adla yeniden abone olmak ya da başka bir ad eklemek isteği değiştirmez. Dinleyenler sıralı tutulduğu için iki sıra aynı listeyi verir ve her iki paylaşımlı işlem de sağlamdır. Sayım iki sağlam, bir yinelemeye duyarlı.

ES19. Dinleyen listesi sıralı tutuldu; abone sırasının sonucu değiştirdiği bir akış ölçülmedi. Paylaşımlı akışın ilk abone ayrıldıktan sonra yeniden başlayıp başlamadığı da ölçülmedi.

Ardışık Tekrarı Düşürmek

removeDuplicates işleci, bir değer son değerle aynıysa onu geçirmez. İşleci iki değerle ölçüyoruz: 3 ve 5.

// main.swift — ardisik tekrari dusuren islec
let fresh = Output(emitted: [])

let distinct = replay("tekrar-dusurme", from: fresh, emitting(3), then: emitting(5)).report()

countSensitivities([distinct])
tekrar-dusurme | bir=[3] iki=[3] ab=[3, 5] ba=[5, 3] -> siraya-duyarli
saglam: 0
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

İkinci 3 düşürülür, bu yüzden bir ile iki aynıdır. Ama değerlerin geliş sırası çıktının sırasıdır: ab [3, 5], ba [5, 3]. İşlem sıraya duyarlıdır. Sayım bir sıraya duyarlı.

ES20. Ölçüm yalnız ardışık tekrarı düşüren işleci modeller. Gecikmeli tetikleme gibi zamana bağlı işleçler sonuç zamanlamaya bağlı olduğu için ölçülmedi.

Toplayan ve Son Değeri Tutan İşleçler

Akışlarda iki işleç çok kullanılır. Biri gelen değerleri toplayıp her adımda toplamı yayımlayan işleçtir; sayaçlar, sepet tutarları ve okunmamış mesaj sayıları böyle kurulur. Diğeri yalnız son değeri tutan işleçtir; ekranda gösterilen alanın değeri, bir ayarın geçerli hâli gibi. İkisi aynı iki değerle (3 ve 5) ve aynı dört koşumla ölçülür. Durum burada düz bir tamsayıdır.

// main.swift — toplayan ve son degeri tutan islec
let running = replay("toplama", from: 0, { $0 + 3 }, then: { $0 + 5 }).report()
let latest = replay("son-deger", from: 0, { _ in 3 }, then: { _ in 5 }).report()

countSensitivities([running, latest])
toplama | bir=3 iki=6 ab=8 ba=8 -> yinelemeye-duyarli
son-deger | bir=3 iki=3 ab=5 ba=3 -> siraya-duyarli
saglam: 0
yinelemeye-duyarli: 1
siraya-duyarli: 1
cift-duyarli: 0

Toplama satırında bir 3, iki 6’dır: aynı değer iki kez gelirse toplam iki katı artar. Sıra önemli değildir; 3 sonra 5 de, 5 sonra 3 de sekiz verir. İşlem yinelemeye duyarlıdır. Son değer satırında aynı değerin ikinci gelişi bir şey değiştirmez, ama son gelen kazandığı için ab 5, ba 3’tür. İşlem sıraya duyarlıdır. Aynı akış iki farklı iş görebilir ve hangi işleçle kurulduğu duyarlılığı belirler.

Gerçek uygulamada bu fark şöyle görünür. Bir yayımcı ağ katmanında yeniden bağlanırken aynı olayı iki kez iletirse, toplayan bir gösterge (okunmamış sayısı) iki kez artar ve yanlış kalır; son değeri tutan bir alan ise aynı değeri iki kez yazar ve ekranda fark edilmez. Yinelenen olayın tehlikesi, işlecin türüne göre ölçülür. Sayaç gibi yinelemeye duyarlı bir işleç kullanıyorsanız, olayları ayırt eden bir kimlik taşımak ve işleçten önce tekrarı düşürmek gerekir.

ES21. Toplama ve son değer ölçümü tamsayı durumu kullanır. Toplamın taşması ve boş akışta ilk değerin ne olacağı ölçülmedi.

Gerçek Akış

Gerçek akış bir çerçeveden gelir ve ölçülen model onun yerine geçmez. Aşağıdaki parça yalnız modelin neyi temsil ettiğini gösterir.

import Combine

let subject = PassthroughSubject<Int, Never>()
let cancellable = subject
    .removeDuplicates()
    .sink { value in print(value) }

subject.send(3)
subject.send(3)
subject.send(5)

Burada PassthroughSubject bir yayımcıdır, removeDuplicates işleçtir, sink aboneliktir. Ardışık iki 3’ten yalnız biri aboneye ulaşır. Abonelik saklanmazsa akış hemen biter; bu yüzden cancellable bir değişkende tutulur.

Gerçek Uygulamada

Bir arama ekranında akış tipik olarak şöyle kurulur: kutudaki metin bir yayımcıdır; ardışık aynı metinleri düşüren işleç, kullanıcı aynı metni yeniden yazdığında isteği önler; bir gecikmeli tetikleme işleci, kullanıcı yazmayı bırakana dek bekler; son olarak bir abone isteği gönderir ve sonucu ekrana yazar. Her halka bu dersin bir ölçümüne karşılık gelir. Tekrar düşürme sıraya duyarlıdır, çünkü metinlerin geliş sırası önemlidir. Abonelik yinelemeye duyarlıdır, çünkü her abonelik bir isteği yeniden başlatabilir. Ekrana yazım ise önceki dersteki sürüm sorusunu yeniden açar.

En sık görülen kurulum hatası aboneliği her görünüşte yeniden kurmaktır.

// Yanlis: ekran her gorundugunde yeni abonelik, eskisi birakilmiyor
@MainActor
final class ListScreen {
    private var subscriptions: [AnyCancellable] = []

    func screenAppeared(_ feed: AnyPublisher<[String], Never>) {
        feed
            .sink { items in self.show(items) }
            .store(in: &subscriptions)
    }

    func show(_ items: [String]) {}
}

Kullanıcı ekrana her döndüğünde screenAppeared yeni bir abonelik ekler ve eskileri bırakmaz. Paylaşımsız bir yayımcıda her abonelik isteği yeniden gönderir; üç kez döndükten sonra aynı değer ekrana üç kez yazılır ve ağa üç istek gider. Bu, ölçülen paylasimsiz-ayni-ad satırının ekran karşılığıdır: aynı adla ikinci abone olmak isteği artırdı. Düzeltmenin iki biçimi vardır. Abonelik bir kez, ekranın oluşturulduğu yerde kurulur; ya da her görünüşte kurulacaksa önce eskisi bırakılır. Paylaşımlı bir yayımcı kullanmak da isteği tek yapar, ama iki ekranın aynı akışı ayrı ömürlerle dinlemesi durumunda ilk abonenin ayrılmasıyla akışın ne olacağı bu derste ölçülmedi.

Bu parça ölçülmedi: gerçek aboneliğin ömrü, bırakma zamanı ve yayımcının soğuk ya da paylaşımlı oluşu çerçevenin davranışına bağlıdır.

Sık Yapılan Hatalar

  • Aboneliği bir değişkende tutmamak. Abonelik serbest kalınca akış iptal olur.
  • Paylaşımsız akışa birden çok kez abone olup isteğin bir kez gittiğini sanmak.
  • Ardışık tekrarı düşürmenin sıra bağımsız olduğunu sanmak. İşleç sırayı değil, ardışıklığı bozar.
  • Ekranı güncelleyen aboneyi ana aktör dışında çalıştırmak.

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

Aynı işleç, değer çiftine göre farklı sınıfa düşer. Çift 3 ve 3 ise iki değer de aynıdır; çift 3 ve 5 ise farklıdır.

// main.swift — ayni islec, iki deger cifti
let fresh = Output(emitted: [])

let equalPair = replay("cift-3-3", from: fresh, emitting(3), then: emitting(3)).report()
let unequalPair = replay("cift-3-5", from: fresh, emitting(3), then: emitting(5)).report()

countSensitivities([equalPair, unequalPair])
cift-3-3 | bir=[3] iki=[3] ab=[3] ba=[3] -> saglam
cift-3-5 | bir=[3] iki=[3] ab=[3, 5] ba=[5, 3] -> siraya-duyarli
saglam: 1
yinelemeye-duyarli: 0
siraya-duyarli: 1
cift-duyarli: 0

Aynı değerler ardışık gelince işleç ikincisini düşürür ve dört durum eşit çıkar: işlem sağlam. Farklı değerlerde sıra çıktıya yazılır ve işlem sıraya duyarlı olur. Kural nedeni görmez, eşitsizliği görür; işleci değil, işleçle birlikte gelen değer çiftini sınıflar. Bir işlecin sağlam olduğunu söylemeden önce hangi değerlerle denendiği yazılmalıdır. Sayım bir sağlam, bir sıraya duyarlı.

Akış mı, Eşzamansız İşlev mi

Akış her sorunun yanıtı değildir. Tek seferlik bir istek için eşzamansız işlev daha az kavram ve daha okunur bir hata yolu sunar. Akış, değerin tekrar tekrar geldiği ve birden çok yerin aynı değerleri dinlediği durumlarda yerini hak eder. Bir ayarın değişimlerini birkaç ekrana dağıtmak buna örnektir; bir profil bilgisini bir kez çekmek değildir.

Karışık kullanımda en sık hata, tek seferlik bir sonucu akışa sarıp ardından ilk değeri beklemektir. Bu hem bir abonelik ömrü sorunu getirir hem de hata yolunu iki katına çıkarır. Ters yönde de hata vardır: değerler sürekli geliyorsa ve kod bunları bir for döngüsüyle ilk değerden sonra bırakıyorsa, akışın asıl kazanımı kaybolur.

Bu dersin ölçümlerinin ortak sonucu şudur: akış zamanı kodun içine taşır, ama yineleme ve sıra soruları değişmez. Her işleç için aynı iki soru sorulur: aynı değer iki kez gelirse sonuç değişir mi, iki değer yer değiştirirse sonuç değişir mi. Cevap işleçten işlece farklıdır ve işleç seçmeden önce bu iki soruyu sormak, sonradan ortaya çıkacak hataların çoğunu önler.

Son olarak bir not: akışları sınamak, zamanı denetleyen bir zamanlayıcı gerektirir. Gecikmeli tetikleme gibi zamana bağlı işleçleri gerçek saatle sınamak yavaş ve kırılgandır. Sınama ortamında zamanlayıcıyı elle ilerleten bir düzenek kullanılır; böylece beş saniyelik bekleme, sınamada anlık bir adım olur. Zamanı elle ilerletmek, ayrıca yarış benzeri hataları da yeniden üretilebilir kılar: yanıtın hangi anda geldiği sınamanın içinde yazılı olduğu için hata her koşumda aynı yerde çıkar. Hatayı yeniden üretebilmek, onu düzeltmenin yarısıdır; ikinci yarısı, düzeltmenin gerçekten çalıştığını aynı senaryoyla göstermektir. Bu dersin modeli zamansızdır ve işleçlerin zamana bağlı yönlerini bilerek dışarıda bırakır.

Özet

  • Tepkisel akışta yayımcı değer üretir, abone alır, işleçler dönüştürür ya da eler.
  • Paylaşımsız akışa her abonelik isteği yineler; işlem yinelemeye duyarlıdır.
  • Paylaşımlı akışta istek yalnız ilk abonede gider ve işlem sağlamdır.
  • Ardışık tekrarı düşüren işleç, farklı değerlerde sıraya duyarlıdır.
  • Aynı işleç eşit değer çiftinde sağlam çıkar; sınıfı işleç değil değer çifti belirler.
  • Bu dersin duyarlılığı: yinelemeye duyarlı (paylaşımsız abonelik), sıraya duyarlı (farklı değerler) ve sağlam (paylaşımlı akış).

Sonraki Adım

Eşzamanlı kodun sonuçları ekranda kalıcı olmayabilir; uygulama kapanınca kaybolur. Sonraki konuda ilk ders, Basit Tercih Saklama, iki ekranın aynı tercihi yazdığı bir modeli ölçecek ve kalıcı durumun yinelemeye ve sıraya nasıl tepki verdiğine bakacak.

İ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