Konkurensi Tingkat Lanjut di Swift dengan HoneyBee

Diterbitkan: 2022-03-11

Merancang, menguji, dan memelihara algoritme serentak di Swift itu sulit dan mendapatkan detail yang benar sangat penting untuk keberhasilan aplikasi Anda. Sebuah algoritma konkuren (juga disebut pemrograman paralel) adalah sebuah algoritma yang dirancang untuk melakukan beberapa (mungkin banyak) operasi pada saat yang sama untuk mengambil keuntungan dari lebih banyak sumber daya perangkat keras dan mengurangi waktu eksekusi secara keseluruhan.

Pada platform Apple, cara tradisional untuk menulis algoritme konkuren adalah NSOperation. Desain NSOperation mengundang programmer untuk membagi algoritma konkuren menjadi tugas-tugas asinkron yang berjalan lama dan individual. Setiap tugas akan didefinisikan dalam subkelasnya sendiri dari NSOperation dan instance dari kelas tersebut akan digabungkan melalui API objektif untuk membuat sebagian urutan tugas saat runtime. Metode merancang algoritme konkuren ini adalah yang tercanggih di platform Apple selama tujuh tahun.

Pada tahun 2014 Apple memperkenalkan Grand Central Dispatch (GCD) sebagai langkah maju yang dramatis dalam ekspresi operasi bersamaan. GCD, bersama dengan blok fitur bahasa baru yang menyertai dan mendukungnya, menyediakan cara untuk mendeskripsikan penangan respons asinkron secara ringkas segera setelah permintaan asinkron memulai. Pemrogram tidak lagi didorong untuk menyebarkan definisi tugas bersamaan di banyak file di banyak subkelas NSOperation. Sekarang, seluruh algoritma konkuren mungkin dapat ditulis dalam satu metode. Peningkatan ekspresi dan keamanan tipe ini merupakan pergeseran konseptual yang signifikan ke depan. Algoritme yang khas dari cara penulisan ini mungkin terlihat seperti berikut:

 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) } } } } }

Mari kita uraikan sedikit algoritma ini. Fungsi processImageData adalah fungsi asinkron yang membuat empat panggilan asinkronnya sendiri untuk menyelesaikan pekerjaannya. Empat pemanggilan async disarangkan satu di dalam yang lain dengan cara yang paling alami untuk penanganan async berbasis blok. Blok hasil masing-masing memiliki parameter Kesalahan opsional dan semua kecuali satu berisi parameter opsional tambahan yang menandakan hasil operasi aysnc.

Bentuk blok kode di atas mungkin tampak familier bagi sebagian besar pengembang Swift. Tapi apa yang salah dengan pendekatan ini? Daftar poin nyeri berikut mungkin akan sama-sama familiar.

  • Bentuk blok kode bersarang "piramida malapetaka" ini dapat dengan cepat menjadi berat. Apa yang terjadi jika kita menambahkan dua operasi asinkron lagi? Empat? Bagaimana dengan operasi bersyarat? Bagaimana dengan perilaku coba lagi atau perlindungan untuk batasan sumber daya? Kode dunia nyata tidak pernah sebersih dan sesederhana contoh dalam posting blog. Efek “piramida malapetaka” dapat dengan mudah menghasilkan kode yang sulit dibaca, sulit dipelihara, dan rentan terhadap bug.
  • Upaya penanganan kesalahan dalam contoh di atas, meskipun Swifty, pada kenyataannya, tidak lengkap. Pemrogram berasumsi bahwa blok callback asinkron gaya Objective-C dua parameter akan selalu menyediakan salah satu dari dua parameter; keduanya tidak akan pernah menjadi nol pada saat yang bersamaan. Ini bukan asumsi yang aman. Algoritma konkuren terkenal karena sulit untuk ditulis dan di-debug, dan asumsi yang tidak berdasar adalah bagian dari alasannya. Penanganan kesalahan yang lengkap dan benar adalah kebutuhan yang tak terhindarkan untuk setiap algoritma konkuren yang bermaksud untuk beroperasi di dunia nyata.
  • Mengambil pemikiran ini lebih jauh, mungkin programmer yang menulis fungsi async yang disebut tidak berprinsip seperti Anda. Bagaimana jika ada kondisi di mana fungsi yang dipanggil gagal dipanggil kembali? Atau menelepon kembali lebih dari sekali? Apa yang terjadi dengan kebenaran processImageData dalam situasi ini? Pro tidak mengambil risiko. Fungsi mission-critical harus benar bahkan ketika mereka bergantung pada fungsi yang ditulis oleh pihak ketiga.
  • Mungkin yang paling menarik, algoritme asinkron yang dipertimbangkan dibangun secara suboptimal. Dua operasi asinkron pertama adalah unduhan sumber daya jarak jauh. Meskipun tidak memiliki interdependensi, algoritma di atas mengeksekusi unduhan secara berurutan dan tidak paralel. Alasan untuk ini jelas; sintaks blok bersarang mendorong pemborosan tersebut. Pasar kompetitif tidak mentolerir kelambatan yang tidak perlu. Jika aplikasi Anda tidak melakukan operasi asinkronnya secepat mungkin, aplikasi lain akan melakukannya.

Bagaimana kita bisa lebih baik? HoneyBee adalah library futures/promises yang membuat pemrograman konkuren Swift menjadi mudah, ekspresif, dan aman. Mari kita tulis ulang algoritma async di atas dengan HoneyBee dan periksa hasilnya:

 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) } }

Baris pertama implementasi ini dimulai adalah resep HoneyBee baru. Baris kedua menetapkan penangan kesalahan default. Penanganan kesalahan tidak opsional dalam resep HoneyBee. Jika ada yang salah, algoritma harus menanganinya. Baris ketiga membuka cabang yang memungkinkan eksekusi paralel. Kedua rantai loadWebResource akan dijalankan secara paralel dan hasilnya akan digabungkan (baris 5). Nilai gabungan dari dua sumber daya yang dimuat diteruskan ke decodeImage dan seterusnya hingga selesai dipanggil.

Mari menelusuri daftar poin nyeri di atas dan melihat bagaimana HoneyBee telah meningkatkan kode ini. Mempertahankan fungsi ini sekarang jauh lebih mudah. Resep HoneyBee terlihat seperti algoritme yang diungkapkannya. Kode ini dapat dibaca, dimengerti, dan dapat dimodifikasi dengan cepat. Desain HoneyBee memastikan bahwa setiap kesalahan pengurutan instruksi menghasilkan kesalahan waktu kompilasi, bukan kesalahan waktu proses. Fungsinya sekarang jauh lebih rentan terhadap bug dan kesalahan manusia.

Semua kemungkinan kesalahan runtime telah sepenuhnya ditangani. Setiap tanda tangan fungsi yang didukung HoneyBee (ada 38 di antaranya) dijamin sepenuhnya ditangani. Dalam contoh kita, callback dua parameter gaya Objective-C akan menghasilkan kesalahan non-nil yang akan dialihkan ke penangan kesalahan, atau akan menghasilkan nilai non-nil yang akan berlanjut ke bawah rantai, atau jika keduanya nilainya nihil HoneyBee akan menghasilkan kesalahan yang menjelaskan bahwa panggilan balik fungsi tidak memenuhi kontraknya.

HoneyBee juga menangani kebenaran kontrak untuk berapa kali panggilan balik fungsi dipanggil. Jika suatu fungsi gagal memanggil panggilan baliknya, HoneyBee menghasilkan kegagalan deskriptif. Jika fungsi memanggil panggilan baliknya lebih dari sekali, HoneyBee akan menekan panggilan tambahan dan peringatan log. Kedua respons kesalahan ini (dan lainnya) dapat disesuaikan untuk kebutuhan individu programmer.

Mudah-mudahan, sudah jelas bahwa bentuk processImageData ini memparalelkan unduhan sumber daya dengan benar untuk memberikan kinerja yang optimal. Salah satu tujuan desain terkuat HoneyBee adalah resepnya harus terlihat seperti algoritme yang diungkapkannya.

Jauh lebih baik. Benar? Tapi HoneyBee memiliki lebih banyak untuk ditawarkan.

Berhati-hatilah: Studi kasus berikutnya bukan untuk orang yang lemah hati. Pertimbangkan deskripsi masalah berikut: Aplikasi seluler Anda menggunakan CoreData untuk mempertahankan statusnya. Anda memiliki model NSManagedObject yang disebut Media, yang mewakili aset media yang diunggah ke server back-end Anda. Pengguna diizinkan untuk memilih lusinan item media sekaligus dan mengunggahnya dalam batch ke sistem backend. Media pertama-tama direpresentasikan melalui String referensi, yang harus diubah menjadi objek Media. Untungnya, aplikasi Anda sudah berisi metode pembantu yang melakukan hal itu:

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

Setelah referensi media diubah menjadi objek Media, Anda harus mengunggah item media ke back-end. Sekali lagi Anda memiliki fungsi pembantu yang siap untuk melakukan hal-hal jaringan.

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

Karena pengguna diizinkan untuk memilih lusinan item media sekaligus, perancang UX telah menentukan jumlah umpan balik yang cukup kuat tentang kemajuan unggahan. Persyaratan telah disuling menjadi empat fungsi berikut:

 /// 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 }

Namun, karena referensi media sumber aplikasi Anda yang terkadang kedaluwarsa, manajer bisnis telah memutuskan untuk mengirim pesan "berhasil" kepada pengguna jika setidaknya setengah dari unggahan berhasil. Artinya, bahwa proses bersamaan harus mendeklarasikan kemenangan—dan memanggil totalProcessSuccess —jika kurang dari setengah dari upaya unggahan yang gagal. Ini adalah spesifikasi yang diberikan kepada Anda sebagai pengembang. Namun sebagai programmer berpengalaman, Anda menyadari bahwa ada lebih banyak persyaratan yang harus diterapkan.

Tentu saja, Business ingin agar unggahan batch terjadi secepat mungkin, jadi pengunggahan serial tidak mungkin dilakukan. Upload harus dilakukan secara paralel.

Tapi tidak terlalu banyak. Jika Anda tanpa pandang bulu async seluruh kumpulan, lusinan unggahan bersamaan akan membanjiri NIC seluler (kartu antarmuka jaringan), dan unggahan sebenarnya akan berjalan lebih lambat daripada secara serial, bukan lebih cepat.

Koneksi jaringan seluler tidak dianggap stabil. Bahkan transaksi singkat mungkin gagal hanya karena perubahan konektivitas jaringan. Untuk benar-benar menyatakan bahwa unggahan telah gagal, kami harus mencoba kembali mengunggah setidaknya sekali.

Kebijakan coba lagi tidak boleh menyertakan operasi ekspor karena tidak tunduk pada kegagalan sementara.

Proses ekspor terikat dengan komputasi dan oleh karena itu harus dilakukan di luar utas utama.

Karena ekspor terikat dengan komputasi, itu harus memiliki jumlah instans bersamaan yang lebih sedikit daripada proses unggahan lainnya untuk menghindari pemusnahan prosesor.

Empat fungsi panggilan balik yang dijelaskan di atas semuanya memperbarui UI, dan semuanya harus dipanggil di utas utama.

Media adalah NSManagedObject , yang berasal dari NSManagedObjectContext dan memiliki persyaratan threading sendiri yang harus dihormati.

Apakah spesifikasi masalah ini tampak agak kabur? Jangan heran jika Anda menemukan masalah seperti ini mengintai di masa depan Anda. Saya menemukan satu seperti ini dalam pekerjaan saya sendiri. Mari kita coba memecahkan masalah ini dengan alat tradisional. Gesper, ini tidak akan cantik.

 /// 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() } } } }

Wow! Tanpa komentar, itu sekitar 75 baris. Apakah Anda mengikuti alasannya sampai tuntas? Bagaimana perasaan Anda jika Anda bertemu monster ini minggu pertama Anda di pekerjaan baru? Apakah Anda merasa siap untuk mempertahankannya, atau memodifikasinya? Apakah Anda tahu jika itu berisi kesalahan? Apakah itu mengandung kesalahan?

Sekarang, pertimbangkan alternatif HoneyBee:

 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)

Bagaimana bentuk ini menyerang Anda? Mari kita kerjakan sepotong demi sepotong. Di baris pertama, kami memulai resep HoneyBee, dimulai dari utas utama. Dengan memulai di utas utama, kami memastikan bahwa semua kesalahan akan diteruskan ke errorHandler (baris 2) di utas utama. Baris 3 menyisipkan larik mediaReferences ke dalam rantai proses. Selanjutnya, kita beralih ke antrian latar belakang global dalam persiapan untuk beberapa paralelisme. Pada baris 5, kita memulai iterasi paralel pada masing-masing mediaReferences . Kami membatasi paralelisme ini hingga maksimal 4 operasi bersamaan. Kami juga menyatakan bahwa iterasi penuh akan dianggap berhasil jika setidaknya setengah dari subchains berhasil (jangan error). Baris 6 mendeklarasikan tautan finally yang akan dipanggil apakah subrantai di bawah ini berhasil atau gagal. Pada tautan finally , kita beralih ke utas utama (baris 7) dan memanggil singleUploadCompletion (baris 8). Pada baris 10, kami menetapkan paralelisasi maksimum 1 (eksekusi tunggal) di sekitar operasi ekspor (baris 11). Baris 13 beralih ke antrean pribadi yang dimiliki oleh instance managedObjectContext kami. Baris 14 mendeklarasikan satu percobaan ulang untuk operasi upload (baris 15). Baris 17 beralih ke utas utama sekali lagi dan 18 memanggil singleUploadSuccess . Pada garis waktu 20 akan dieksekusi, semua iterasi paralel telah selesai. Jika kurang dari setengah dari iterasi yang gagal, maka baris 20 beralih ke antrian utama untuk terakhir kalinya (ingat masing-masing dijalankan pada antrian latar belakang), 21 menjatuhkan nilai masuk (masih mediaReferences ), dan 22 memanggil totalProcessSuccess .

Bentuk HoneyBee lebih jernih, bersih, dan lebih mudah dibaca, serta lebih mudah perawatannya. Apa yang akan terjadi pada bentuk panjang dari algoritme ini jika loop diperlukan untuk mengintegrasikan kembali objek Media ke dalam larik seperti fungsi peta? Setelah Anda melakukan perubahan, seberapa yakin Anda bahwa semua persyaratan algoritme masih terpenuhi? Dalam bentuk HoneyBee, perubahan ini akan menggantikan masing-masing dengan peta untuk menggunakan fungsi peta paralel. (Ya, itu juga berkurang.)

HoneyBee adalah pustaka berjangka yang kuat untuk Swift yang membuat penulisan algoritme asinkron dan konkuren menjadi lebih mudah, lebih aman, dan lebih ekspresif. Dalam artikel ini, kita telah melihat bagaimana HoneyBee dapat membuat algoritme Anda lebih mudah dirawat, lebih benar, dan lebih cepat. HoneyBee juga memiliki dukungan untuk paradigma asinkron kunci lainnya seperti dukungan coba lagi, beberapa penangan kesalahan, penjagaan sumber daya, dan pemrosesan pengumpulan (bentuk peta asinkron, filter, dan pengurangan). Untuk daftar lengkap fitur, lihat situs webnya. Untuk mempelajari lebih lanjut atau mengajukan pertanyaan, lihat forum komunitas baru.

Lampiran: Memastikan Kebenaran Kontrak dari Fungsi Async

Memastikan kebenaran kontraktual dari fungsi adalah prinsip dasar ilmu komputer. Sedemikian rupa sehingga hampir semua kompiler modern memiliki pemeriksaan untuk memastikan bahwa fungsi yang menyatakan untuk mengembalikan nilai, kembali tepat sekali. Mengembalikan kurang dari atau lebih dari sekali diperlakukan sebagai kesalahan dan dengan tepat mencegah kompilasi penuh.

Tetapi bantuan kompiler ini biasanya tidak berlaku untuk fungsi asinkron. Perhatikan contoh (menyenangkan) berikut ini:

 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") } }

Fungsi generateIcecream menerima Int dan secara asinkron mengembalikan sebuah String. Kompiler Swift dengan senang hati menerima formulir di atas sebagai benar, meskipun berisi beberapa masalah yang jelas. Mengingat input tertentu, fungsi ini mungkin memanggil penyelesaian nol, satu, atau dua kali. Pemrogram yang telah bekerja dengan fungsi async sering akan mengingat contoh masalah ini dalam pekerjaan mereka sendiri. Apa yang bisa kita lakukan? Tentu saja, kami dapat memfaktorkan ulang kode menjadi lebih rapi (switch dengan range case akan berfungsi di sini). Tetapi terkadang kompleksitas fungsional sulit untuk dikurangi. Bukankah lebih baik jika kompiler dapat membantu kami dalam memverifikasi kebenaran seperti halnya dengan mengembalikan fungsi secara teratur?

Ternyata ada caranya. Perhatikan mantra Swifty berikut ini:

 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") } }

Empat baris yang disisipkan di bagian atas fungsi ini memaksa kompiler untuk memverifikasi bahwa panggilan balik penyelesaian dipanggil tepat satu kali, yang berarti bahwa fungsi ini tidak lagi dikompilasi. Apa yang sedang terjadi? Pada baris pertama, kami mendeklarasikan tetapi tidak menginisialisasi hasil yang pada akhirnya kami inginkan untuk dihasilkan oleh fungsi ini. Dengan membiarkannya tidak terdefinisi, kami memastikan bahwa itu harus ditetapkan satu kali sebelum dapat digunakan, dan dengan mendeklarasikannya, kami memastikan bahwa itu tidak pernah dapat ditetapkan dua kali. Baris kedua adalah penangguhan yang akan dieksekusi sebagai tindakan terakhir dari fungsi ini. Ini memanggil blok penyelesaian dengan finalResult - setelah ditugaskan oleh fungsi lainnya. Baris 3 membuat konstanta baru yang disebut penyelesaian yang membayangi parameter panggilan balik. Penyelesaian baru adalah tipe Void yang menyatakan tidak ada API publik. Baris ini memastikan bahwa setiap penggunaan penyelesaian setelah baris ini akan menjadi kesalahan kompiler. Penundaan pada baris 2 adalah satu-satunya penggunaan blok penyelesaian yang diizinkan. Baris 4 menghapus peringatan kompiler yang seharusnya ada tentang konstanta penyelesaian baru yang tidak digunakan.

Jadi kami telah berhasil memaksa kompiler Swift untuk melaporkan bahwa fungsi asinkron ini tidak memenuhi kontraknya. Mari kita berjalan melalui langkah-langkah untuk membuatnya benar. Pertama, mari kita ganti semua akses langsung ke panggilan balik dengan tugas ke finalResult .

 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" } }

Sekarang kompiler melaporkan dua masalah:

 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"

Seperti yang diharapkan, fungsi tersebut memiliki jalur di mana finalResult ditetapkan nol kali dan juga jalur di mana ia ditetapkan lebih dari sekali. Kami menyelesaikan masalah ini sebagai berikut:

 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" } }

The "Pistachio" telah dipindahkan ke klausa lain yang tepat dan kami menyadari bahwa kami gagal untuk menutupi kasus umum-yang tentu saja adalah "Neapolitan."

Pola yang baru saja dijelaskan dapat dengan mudah disesuaikan untuk mengembalikan nilai opsional, kesalahan opsional, atau tipe kompleks seperti enum Hasil umum. Dengan memaksa kompiler untuk memverifikasi bahwa panggilan balik dipanggil tepat satu kali, kita dapat menegaskan kebenaran dan kelengkapan fungsi asinkron.