HoneyBee ile Swift'de Gelişmiş Eşzamanlılık

Yayınlanan: 2022-03-11

Swift'de eşzamanlı algoritmaları tasarlamak, test etmek ve sürdürmek zordur ve ayrıntıları doğru bir şekilde elde etmek uygulamanızın başarısı için kritik öneme sahiptir. Eşzamanlı algoritma (paralel programlama olarak da adlandırılır), daha fazla donanım kaynağından yararlanmak ve genel yürütme süresini azaltmak için aynı anda birden çok (belki de birçok) işlemi gerçekleştirmek üzere tasarlanmış bir algoritmadır.

Apple platformlarında, eşzamanlı algoritmalar yazmanın geleneksel yolu NSOperation'dır. NSOperation'ın tasarımı, programcıyı eşzamanlı bir algoritmayı uzun süreli, asenkron görevlere ayırmaya davet eder. Her görev, kendi NSOperation alt sınıfında tanımlanacak ve bu sınıfların örnekleri, çalışma zamanında kısmi bir görev sırası oluşturmak için nesnel bir API aracılığıyla birleştirilecektir. Bu eşzamanlı algoritmalar tasarlama yöntemi, yedi yıl boyunca Apple platformlarında en son teknolojiydi.

2014'te Apple, Grand Central Dispatch'i (GCD) eşzamanlı operasyonların ifadesinde ileriye doğru dramatik bir adım olarak tanıttı. GCD, kendisine eşlik eden ve ona güç veren yeni dil özellik bloklarıyla birlikte, eşzamansız istek başlatıldıktan hemen sonra eşzamansız yanıt işleyicisini kompakt bir şekilde tanımlamanın bir yolunu sağladı. Artık programcılar, eşzamanlı görevlerin tanımını çok sayıda NSOperation alt sınıfında birden çok dosyaya yaymaya teşvik edilmedi. Şimdi, tüm bir eşzamanlı algoritma, uygulanabilir bir şekilde tek bir yöntem içinde yazılabilir. Anlamlılık ve tip güvenliğindeki bu artış, ileriye dönük önemli bir kavramsal kaymaydı. Bu yazma yöntemine özgü bir algoritma aşağıdaki gibi görünebilir:

 func processImageData(completion: (result: Image?, error: Error?) -> Void) { loadWebResource("dataprofile.txt") { (dataResource, error) in guard let dataResource = dataResource else { completion(nil, error) return } loadWebResource("imagedata.dat") { (imageResource, error) in guard let imageResource = imageResource else { completion(nil, error) return } decodeImage(dataResource, imageResource) { (imageTmp, error) in guard let imageTmp = imageTmp else { completion(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult in guard let imageResult = imageResult else { completion(nil, error) return } completion(imageResult, nil) } } } } }

Bu algoritmayı biraz parçalayalım. processImageData işlevi, işini tamamlamak için kendi başına dört eşzamansız çağrı yapan eşzamansız bir işlevdir. Dört zaman uyumsuz çağrı, blok tabanlı zaman uyumsuz işleme için en doğal olan şekilde iç içe yerleştirilmiştir. Sonuç bloklarının her biri isteğe bağlı bir Hata parametresine sahiptir ve biri hariç tümü aysnc işleminin sonucunu belirten ek bir isteğe bağlı parametre içerir.

Yukarıdaki kod bloğunun şekli muhtemelen çoğu Swift geliştiricisine tanıdık gelecektir. Ama bu yaklaşımda yanlış olan ne? Aşağıdaki ağrı noktaları listesi muhtemelen aynı derecede tanıdık olacaktır.

  • İç içe kod bloklarının bu "kıyamet piramidi" şekli hızla hantal hale gelebilir. İki async işlemi daha eklersek ne olur? Dört mü? Peki ya koşullu işlemler? Yeniden deneme davranışına veya kaynak sınırları için korumalara ne dersiniz? Gerçek dünya kodu, hiçbir zaman blog yazılarındaki örnekler kadar temiz ve basit değildir. "Kıyamet piramidi" etkisi, kolayca okunması zor, bakımı zor ve hatalara açık kodlarla sonuçlanabilir.
  • Yukarıdaki örnekte hata işleme girişimi Swifty olmasına rağmen aslında eksiktir. Programcı, iki parametreli, Objective-C stili zaman uyumsuz geri arama bloklarının her zaman iki parametreden birini sağlayacağını varsaymıştır; ikisi aynı anda asla sıfır olmayacak. Bu güvenli bir varsayım değildir. Eşzamanlı algoritmalar, yazması ve hata ayıklaması zor olmakla ünlüdür ve asılsız varsayımlar bunun bir parçasıdır. Eksiksiz ve doğru hata işleme, gerçek dünyada çalışmayı amaçlayan herhangi bir eşzamanlı algoritma için kaçınılmaz bir zorunluluktur.
  • Bu düşünceyi daha da ileri götürerek, belki de async denilen işlevleri yazan programcı sizin kadar ilkeli değildi. Çağrılan işlevlerin geri aramayı başaramadığı koşullar varsa ne olur? Veya bir kereden fazla geri aramak mı? Bu koşullar altında processImageData'nın doğruluğuna ne olur? Profesyoneller risk almaz. Görev açısından kritik işlevlerin, üçüncü taraflarca yazılan işlevlere dayansalar bile doğru olması gerekir.
  • Belki de en zorlayıcı, kabul edilen zaman uyumsuz algoritma, optimal olmayan bir şekilde oluşturulmuştur. İlk iki zaman uyumsuz işlemin ikisi de uzak kaynakların indirilmesidir. Birbirlerine bağımlılıkları olmamasına rağmen, yukarıdaki algoritma indirmeleri paralel değil sıralı olarak yürütür. Bunun nedenleri açıktır; iç içe blok sözdizimi bu tür israfı teşvik eder. Rekabetçi piyasalar gereksiz gecikmeye tolerans göstermez. Uygulamanız eşzamansız işlemlerini olabildiğince hızlı gerçekleştirmezse, başka bir uygulama yapacaktır.

Nasıl daha iyi yapabiliriz? HoneyBee, Swift eşzamanlı programlamayı kolay, anlamlı ve güvenli hale getiren bir vadeli işlemler/vaatler kitaplığıdır. HoneyBee ile yukarıdaki async algoritmasını yeniden yazalım ve sonucu inceleyelim:

 func processImageData(completion: (result: Image?, error: Error?) -> Void) { HoneyBee.start() .setErrorHandler { completion(nil, $0) } .branch { stem in stem.chain(loadWebResource =<< "dataprofile.txt") + stem.chain(loadWebResource =<< "imagedata.dat") } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion($0, nil) } }

Bu uygulamanın başladığı ilk satır, yeni bir HoneyBee tarifi. İkinci satır, varsayılan hata işleyicisini oluşturur. HoneyBee tariflerinde hata işleme isteğe bağlı değildir. Bir şeyler ters gidebilirse, algoritmanın bunu halletmesi gerekir. Üçüncü satır, paralel yürütmeye izin veren bir dal açar. İki loadWebResource zinciri paralel olarak yürütülecek ve sonuçları birleştirilecektir (5. satır). Yüklenen iki kaynağın birleşik değerleri, decodeImage iletilir ve tamamlama çağrılana kadar zincir boyunca devam eder.

Yukarıdaki sorunlu noktalar listesini gözden geçirelim ve HoneyBee'nin bu kodu nasıl geliştirdiğini görelim. Bu işlevi sürdürmek artık çok daha kolay. HoneyBee tarifi, ifade ettiği algoritmaya benziyor. Kod okunabilir, anlaşılabilir ve hızlı bir şekilde değiştirilebilir. HoneyBee'nin tasarımı, talimatların herhangi bir yanlış sıralanmasının, bir çalışma zamanı hatası değil, bir derleme zamanı hatasıyla sonuçlanmasını sağlar. İşlev artık hatalara ve insan hatasına karşı çok daha az hassastır.

Tüm olası çalışma zamanı hataları tamamen işlendi. HoneyBee'nin desteklediği her fonksiyon imzasının (bunlardan 38 tanesi vardır) tam olarak işlendiği garanti edilir. Örneğimizde, Objective-C stili iki parametreli geri arama, ya hata işleyiciye yönlendirilecek sıfır olmayan bir hata üretecek ya da zincirde ilerleyecek sıfır olmayan bir değer üretecek ya da her ikisi birden değerler sıfırdır HoneyBee, geri arama işlevinin sözleşmesini yerine getirmediğini açıklayan bir hata üretecektir.

HoneyBee ayrıca, işlev geri aramalarının çağrılma sayısı için sözleşmeye dayalı doğruluğu da ele alır. Bir işlev geri çağrısını başlatamazsa, HoneyBee açıklayıcı bir hata üretir. İşlev geri çağrısını birden fazla kez çalıştırırsa, HoneyBee yardımcı çağrıları bastırır ve uyarıları günlüğe kaydeder. Bu hata yanıtlarının her ikisi (ve diğerleri) programcının bireysel ihtiyaçlarına göre özelleştirilebilir.

Umarım, bu processImageData biçiminin, optimum performansı sağlamak için kaynak indirmelerini düzgün bir şekilde paralelleştirdiği zaten açık olmalıdır. HoneyBee'nin en güçlü tasarım hedeflerinden biri, tarifin ifade ettiği algoritmaya benzemesi gerektiğidir.

Çok daha iyi. Doğru? Ancak HoneyBee'nin sunabileceği daha çok şey var.

Dikkatli olun: Bir sonraki vaka çalışması, kalbin zayıflığı için değil. Aşağıdaki sorun açıklamasını göz önünde bulundurun: Mobil uygulamanız, durumunu sürdürmek için CoreData kullanır. Arka uç sunucunuza yüklenen bir medya varlığını temsil eden Media adlı bir NSManagedObject modeliniz var. Kullanıcının aynı anda düzinelerce medya öğesini seçmesine ve bunları toplu olarak arka uç sistemine yüklemesine izin verilecektir. Medya ilk önce bir Medya nesnesine dönüştürülmesi gereken bir referans Dizesi aracılığıyla temsil edilir. Neyse ki, uygulamanız zaten tam da bunu yapan bir yardımcı yöntem içeriyor:

 func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

Medya referansı bir Medya nesnesine dönüştürüldükten sonra medya öğesini arka uca yüklemelisiniz. Yine ağ işlerini yapmaya hazır bir yardımcı fonksiyonunuz var.

 func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

Kullanıcının aynı anda düzinelerce medya öğesini seçmesine izin verildiğinden, UX tasarımcısı yükleme ilerlemesi hakkında oldukça sağlam miktarda geri bildirim belirlemiştir. Gereksinimler aşağıdaki dört işleve ayrılmıştır:

 /// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

Ancak, uygulamanız bazen süresi dolan medya referanslarına kaynak sağladığından, işletme yöneticileri, yüklemelerin en az yarısının başarılı olması durumunda kullanıcıya bir "başarılı" mesajı göndermeye karar vermiştir. Yani, denenen yüklemelerin yarısından azı başarısız olursa, eşzamanlı süreç zafer ilan etmeli ve totalProcessSuccess . Bu, geliştirici olarak size verilen özelliktir. Ancak deneyimli bir programcı olarak, uygulanması gereken daha fazla gereksinim olduğunun farkındasınız.

Elbette Business, toplu yüklemenin mümkün olduğunca çabuk gerçekleşmesini istiyor, bu nedenle seri yükleme söz konusu değil. Yüklemeler paralel olarak gerçekleştirilmelidir.

Ama çok fazla değil. Tüm toplu işi gelişigüzel bir şekilde async hale getirirseniz, düzinelerce eşzamanlı yükleme mobil NIC'yi (ağ arabirim kartı) doldurur ve yüklemeler aslında seriden daha yavaş ilerler, daha hızlı değil.

Mobil ağ bağlantıları kararlı olarak kabul edilmez. Kısa işlemler bile yalnızca ağ bağlantısındaki değişiklikler nedeniyle başarısız olabilir. Bir yüklemenin başarısız olduğunu gerçekten beyan etmek için yüklemeyi en az bir kez yeniden denememiz gerekir.

Yeniden deneme ilkesi, geçici hatalara maruz kalmadığından dışa aktarma işlemini içermemelidir.

Dışa aktarma işlemi hesaplamaya bağlıdır ve bu nedenle ana iş parçacığından gerçekleştirilmelidir.

Dışa aktarma işlemi hesaplamaya bağlı olduğundan, işlemcinin thrash edilmesini önlemek için geri kalan yükleme işleminden daha az sayıda eşzamanlı örneğe sahip olmalıdır.

Yukarıda açıklanan dört geri arama işlevinin tümü, kullanıcı arabirimini günceller ve bu nedenle tümü ana iş parçacığında çağrılmalıdır.

Medya, bir NSManagedObject gelen ve uyulması gereken kendi iş parçacığı gereksinimlerine sahip bir NSManagedObjectContext .

Bu sorun belirtimi biraz belirsiz görünüyor mu? Geleceğinizde bunun gibi sorunlarla karşılaşırsanız şaşırmayın. Kendi çalışmamda böyle biriyle karşılaştım. Önce bu sorunu geleneksel araçlarla çözmeye çalışalım. Kemerlerinizi bağlayın, bu hiç hoş olmayacak.

 /// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts < uploadRetries { uploadAttempts += 1 doUpload() // retry } else { DispatchQueue.main.async { // too many upload failures errorHandler(error) finalizeMediaRef() } } } else { DispatchQueue.main.async { uploadSuccesses += 1 singleUploadSuccess(media) finalizeMediaRef() } } } } } // kick off the first upload doUpload() } } } }

Vay! Yorumsuz, yaklaşık 75 satır. Mantığını sonuna kadar takip ettin mi? Yeni bir işte ilk haftanızda bu canavarla karşılaşsaydınız nasıl hissederdiniz? Onu korumaya veya değiştirmeye hazır hissediyor musunuz? Hatalar içerip içermediğini biliyor muydunuz? Hatalar içeriyor mu?

Şimdi HoneyBee alternatifini düşünün:

 HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

Bu form size nasıl geliyor? Parça parça üzerinde çalışalım. İlk satırda, ana iş parçacığından başlayarak HoneyBee tarifine başlıyoruz. Ana iş parçacığından başlayarak, tüm hataların ana iş parçacığındaki errorHandler'a (satır 2) iletilmesini sağlıyoruz. Satır 3, mediaReferences dizisini işlem zincirine ekler. Ardından, bazı paralelliklere hazırlık olarak global arka plan kuyruğuna geçiyoruz. 5. satırda, mediaReferences her biri üzerinde paralel bir yinelemeye başlıyoruz. Bu paralelliği maksimum 4 eşzamanlı işlemle sınırlandırıyoruz. Ayrıca, alt zincirlerin en az yarısı başarılı olursa (hata yapma) tam yinelemenin başarılı sayılacağını da beyan ederiz. Satır 6, aşağıdaki alt zincirin başarılı veya başarısız olup olmadığına bakılacak olan bir finally bağlantısını bildirir. finally bağlantıda, ana iş parçacığına (satır 7) geçiyoruz ve singleUploadCompletion (satır 8) çağırıyoruz. 10. satırda, dışa aktarma işlemi (satır 11) çevresinde maksimum 1 paralelleştirme (tek yürütme) belirledik. Satır 13, managedObjectContext sahip olduğu özel kuyruğa geçer. Satır 14, yükleme işlemi için tek bir yeniden deneme girişimi bildirir (satır 15). Satır 17, bir kez daha ana iş parçacığına geçer ve 18, singleUploadSuccess çağırır. Zaman çizgisi 20 yürütüldüğünde, tüm paralel yinelemeler tamamlanmıştır. Yinelemelerin yarısından azı başarısız olursa, 20. satır son bir kez ana kuyruğa geçer (her birinin arka planda çalıştırıldığını hatırlayın), 21 gelen değeri düşürür (hala mediaReferences ) ve 22'si totalProcessSuccess çağırır.

HoneyBee formu daha net, daha temiz ve okunması daha kolay, bakımı daha kolay. Döngünün Medya nesnelerini bir harita işlevi gibi bir diziye yeniden entegre etmesi gerekirse, bu algoritmanın uzun biçimine ne olurdu? Değişikliği yaptıktan sonra, algoritmanın tüm gereksinimlerinin hala karşılandığından ne kadar emin olabilirsiniz? HoneyBee formunda bu değişiklik, paralel bir harita işlevi kullanmak için her birini haritayla değiştirmek olacaktır. (Evet, o da azaldı.)

HoneyBee, asenkron ve eşzamanlı algoritmalar yazmayı daha kolay, daha güvenli ve daha anlamlı hale getiren Swift için güçlü bir gelecek kitaplığıdır. Bu yazıda HoneyBee'nin algoritmalarınızı nasıl daha kolay, daha doğru ve daha hızlı hale getirebileceğini gördük. HoneyBee ayrıca yeniden deneme desteği, çoklu hata işleyiciler, kaynak koruma ve toplama işleme (eşzamansız harita, filtre ve küçültme biçimleri) gibi diğer önemli zaman uyumsuz paradigmaları da destekler. Özelliklerin tam listesi için web sitesine bakın. Daha fazla bilgi edinmek veya soru sormak için yepyeni topluluk forumlarına bakın.

Ek: Zaman Uyumsuz İşlevlerin Sözleşmeye Dayalı Doğruluğunun Sağlanması

İşlevlerin sözleşmeye dayalı doğruluğunu sağlamak, bilgisayar biliminin temel bir ilkesidir. Öyle ki, hemen hemen tüm modern derleyicilerin, bir değer döndüreceğini bildiren bir işlevin tam olarak bir kez dönmesini sağlamak için kontrolleri vardır. Bir kereden daha az veya daha fazla döndürmek bir hata olarak değerlendirilir ve tam bir derlemeyi uygun şekilde engeller.

Ancak bu derleyici yardımı genellikle zaman uyumsuz işlevler için geçerli değildir. Aşağıdaki (eğlenceli) örneği göz önünde bulundurun:

 func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } completion("Pistachio") } else if int < 2 { completion("Vanilla") } }

generateIcecream işlevi bir Int kabul eder ve eşzamansız olarak bir String döndürür. Swift derleyicisi, bazı bariz problemler içermesine rağmen, yukarıdaki formu mutlu bir şekilde doğru olarak kabul eder. Belirli girdiler verildiğinde, bu işlev tamamlamayı sıfır, bir veya iki kez çağırabilir. Zaman uyumsuz işlevlerle çalışan programcılar genellikle bu sorunun örneklerini kendi çalışmalarında hatırlayacaktır. Ne yapabiliriz? Elbette, kodu daha düzgün olacak şekilde yeniden düzenleyebiliriz (aralık durumlarına sahip bir anahtar burada işe yarar). Ancak bazen işlevsel karmaşıklığı azaltmak zordur. Derleyici, tıpkı düzenli olarak dönen işlevlerde olduğu gibi, doğruluğu doğrulamada bize yardımcı olsa daha iyi olmaz mıydı?

Bir yol olduğu ortaya çıkıyor. Aşağıdaki Swifty büyüsüne dikkat edin:

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { completion("Chocolate") } else if int < 10 { completion("Strawberry") } // else completion("Pistachio") } else if int < 2 { completion("Vanilla") } }

Bu işlevin en üstüne eklenen dört satır, derleyiciyi tamamlama geri çağrısının tam olarak bir kez çağrıldığını doğrulamaya zorlar; bu, bu işlevin artık derlenmediği anlamına gelir. Neler oluyor? İlk satırda, sonuçta bu fonksiyonun üretmesini istediğimiz sonucu bildiririz, ancak başlatmayız. Tanımsız bırakarak, kullanılmadan önce bir kez atanmasını sağlarız ve ilan ederek asla iki kez atanmamasını sağlarız. İkinci satır, bu işlevin son eylemi olarak yürütülecek olan bir ertelemedir. finalResult ile tamamlama bloğunu çağırır - işlevin geri kalanı tarafından atandıktan sonra. Satır 3, geri arama parametresini gölgeleyen tamamlama adlı yeni bir sabit oluşturur. Yeni tamamlama, genel API bildirmeyen Void türündedir. Bu satır, bu satırdan sonra herhangi bir tamamlama kullanımının bir derleyici hatası olmasını sağlar. 2. satırdaki erteleme, tamamlama bloğunun izin verilen tek kullanımıdır. Satır 4, aksi takdirde yeni tamamlama sabitinin kullanılmadığı konusunda mevcut olacak bir derleyici uyarısını kaldırır.

Bu nedenle, Swift derleyicisini bu eşzamansız işlevin sözleşmesini yerine getirmediğini bildirmeye başarıyla zorladık. Doğru yapmak için adımlardan geçelim. İlk olarak, geri aramaya tüm doğrudan erişimi, finalResult atamasıyla değiştirelim.

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } // else finalResult = "Pistachio" } else if int < 2 { finalResult = "Vanilla" } }

Şimdi derleyici iki sorun bildiriyor:

 error: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = "Pistachio"

Beklendiği gibi, işlevin finalResult sıfır kez atandığı bir yolu ve ayrıca birden fazla atandığı bir yolu vardır. Bu sorunları şu şekilde çözüyoruz:

 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int < 20 { finalResult = "Chocolate" } else if int < 10 { finalResult = "Strawberry" } else { finalResult = "Pistachio" } } else if int < 2 { finalResult = "Vanilla" } else { finalResult = "Neapolitan" } }

"Fıstık", başka bir uygun maddeye taşındı ve genel durumu -ki tabii ki "Napoliten"i ele almadığımızı fark ettik.

Az önce açıklanan desenler, isteğe bağlı değerler, isteğe bağlı hatalar veya ortak Result enum gibi karmaşık türleri döndürmek için kolayca ayarlanabilir. Derleyiciyi, geri aramaların tam olarak bir kez çağrıldığını doğrulamaya zorlayarak, asenkron işlevlerin doğruluğunu ve eksiksizliğini iddia edebiliriz.