Swift, SwiftUI gibi kütüphaneler için deklaratif sözdizimini sağlamak amacıyla 5.1 sürümünde sonuç oluşturucularını (ilk olarak fonksiyon oluşturucular olarak adlandırılan) tanıttı. Daha önce, hiyerarşik veri yapıları oluşturmak, görsel olarak gürültülü ve bakımı zor olan derinlemesine iç içe geçmiş başlatıcı çağrılarını gerektiriyordu. Bu özellik, analizci kombinator kütüphaneleri ve fonksiyonel programlama monadlarından esinlenerek, Swift'in statik tür sistemi ile uyum sağlamak üzere uyarlanmıştı ve imperatif sözdizimi alışkanlığını korumuştu.
Geliştiricilerin, Swift'in derleme zamanı tür güvenliğinden veya zamanlama aşamasında yük eklemekten ödün vermeden karmaşık değerleri oluşturan ardışık ifadeler yazmanın bir yoluna ihtiyacı vardı. Temel zorluk, bu yapıların içinde farklı dalların farklı türler üretebileceği kontrol akış yapıları (örneğin if ifadeleri ve for döngüleri) desteklemekti. Sadece varoluşsal türlerin dizilerini kullanmak, somut tür bilgilerini kaybettirir ve dinamik dağıtım zorunluluğu doğururdu, bu da performansa kritik olan kod yollarını zayıflatırdı.
Swift derleyicisi, anlamsal analiz aşamasında bir kaynak-kaynağa dönüşüm gerçekleştirerek sonuç oluşturucu kapanış gövdesini oluşturucu türü üzerinde bir dizi statik yöntem çağrısı haline getirir. Ardışık ifadeler buildBlock argümanlarına dönüşür, koşullar buildEither(first:) ve buildEither(second:) çağrılarına dönüştürülür, ve isteğe bağlı dallar buildOptional kullanır. Bu dönüşüm tür kontrolünden önce gerçekleşir, böylece derleyici birleştirilmiş türlerin beklenen dönüş türü ile eşleştiğini doğrularken, elle yapılmış iç içe geçmiş çağrılara eşdeğer verimli çevrimiçi kod üretir.
@resultBuilder struct MyBuilder { static func buildBlock<T1, T2>(_ t1: T1, _ t2: T2) -> (T1, T2) { (t1, t2) } static func buildOptional<T>(_ component: T?) -> T? { component } static func buildEither<T>(first: T) -> T { first } static func buildEither<T>(second: T) -> T { second } } @MyBuilder func build() -> (Int, String?) { 42 if Bool.random() { "hello" } }
Bir arka uç ekibi, akıcı bir arayüz kullanarak veritabanı sorgu boru hatları oluşturmak istedi. Geliştiricilerin, nokta ile yöntemleri zincirlemek yerine dikey olarak işlemleri listeleyebileceği bir sözdizimi istemişlerdi, aynı zamanda şemaların uyumunu derleme zamanında doğrulamayı koruyarak.
Öncelikle her işlemin değiştirilmiş bir Query nesnesi döndürdüğü geleneksel yöntem zincirlemeyi düşünmüşlerdi. Bu yaklaşım basit, lineer boru hatları için işe yaradı ancak filtreleri veya birleştirmeleri koşullu olarak eklemek zorlaştı ve zinciri korumak için geçici değişkenler ve karmaşık üçlü ifadeler gerektirdi. Ayrıca tüm ara türlerin aynı olmasını zorunlu kılarak aşama özgü optimizasyonları engelliyordu.
Diğer bir seçenek, kapanış tabanlı değiştiricilerden oluşan bir dizi kabul etmekti [(Query) -> Query]. Bu, istenen dikey sözdizimini sağladı ancak her adımda tür bilgilerini tamamen silerek, sütun varlığı veya tür uyumsuzluklarının derleme zamanında doğrulanmasını engelledi. Ölçümler, bu durumun 15% zamanlama aşaması yükü getirdiğini gösterdi çünkü dönüşüm kapanışlarını içe almak imkânsızdı.
Ekip, özel bir @QueryBuilder sonuç oluşturucu uyguladı. Heterojen boru hattı aşamalarını kabul etmek ve bunları türlendirilmiş bir olduğu demete birleştirmek için buildBlock yöntemlerini aşırı yüklediler, koşullu WHERE ifadelerini türleri silmeden işlemek için buildEither tanımladılar ve for döngüsü üretimli JOIN işlemleri için buildArray kullandılar. Bu, dikey deklaratif sözdizimini korurken sıfır maliyetli soyutlamalar sağlıyordu ve LLVM optimizasyoncusu boru hattı inşasını inleyebiliyordu. Sorgu tanım kodu %50 daha kısa hale geldi ve şema uyumsuzlukları derleme zamanında entegrasyon testleri sırasında yerine geçmeden yakalandı.
Derleyici, sonuç oluşturucusunda farklı durumların farklı somut türler döndürmesi durumunda bir switch ifadesini nasıl dönüştürüyor?
Derleyici, switch'i iç içe geçmiş buildEither çağrılarından oluşan bir ikili ağaç haline dönüştürüyor ve tür kontrol cihazının tüm dalları tek bir türde birleştirmesini gerektiriyor. Eğer durumlar farklı türler döndürüyorsa (örneğin Text ve Image SwiftUI içinde), derleme başarısız olur, bunun için oluşturucunun tür yok etme sağlaması gerekir. Adaylar genellikle switch'in özel çoklu yönlü dağıtım işleme aldığını varsayıyor, ancak esasen ikili kararlar (ilk durum vs geri kalan) üzerinden geçer. Çözüm, tüm durumların aynı somut türü döndürmesini sağlamak veya değerleri varoluşsal bir kaplayıcıda sarmak için buildExpression uygulamak gereklidir, ancak bu da statik optimizasyon fırsatlarını feda eder.
Sonuç oluşturucusunda bir @available kontrolü eklemek neden buildLimitedAvailability aracılığıyla özel bir işlem gerektirir?
Bir sonuç oluşturucusunda, bir erişilebilirlik denetimi içinde (örneğin if #available(iOS 15, *)) sarmalanmış bir kod varsa, derleyici, korunan blok içindeki bileşenlerin tüm dağıtım hedeflerinde mevcut olduğunu garanti edemez. buildLimitedAvailability olmadan, tür denetleyici başarısız olur çünkü erişilebilirlik korumalı kodu minimum dağıtım hedefi ile doğrulamaya çalışır. Bu yöntem, kurucuya eski OS sürümlerine hedeflenirken bir yer tutucu veya boş değer sağlamasına izin veren bir derleme zamanı filtresi görevi görür. Adaylar genellikle bunun, kullanılmayan kod yollarının ikili oluşturma öncesinde tamamen tür yok edilmiş veya değiştirilmiş olmasını sağlayarak "sembol bulunamadı" bağlantı zamanı hatalarını önlediğini kaçırıyor.
buildExpression ve buildBlock arasındaki kesin fark nedir ve tür güvenliği için buildExpression uygulamak ne zaman gereklidir?
buildBlock, birden fazla zaten dönüştürülmüş bileşeni nihai bir sonuca birleştirirken, buildExpression bireysel ifadelerin buildBlock'a iletilmeden önce dönüştürülmesini sağlayan opsiyonel bir kancadır. Adaylar genellikle buildExpression'ın, bireysel ifadeler seviyesinde erken tür yok etmesine izin vererek, heterojen türlerin birleşmeden önce birleştirilmesini sağladığını kaçırıyor. Örneğin, SwiftUI'nin ViewBuilder'ı, yalnızca gerektiğinde görünümleri AnyView içinde sarmak için buildExpression kullanır veya görünüm değiştiricilerini uygular. Bu ayrımı anlamadan, geliştiricilerin kullanıcıyı her ifadeyi manuel olarak dönüştürmeye zorlamadan, ardışık ifadeler arasındaki tür uyumsuzluklarını zarif bir şekilde yöneten oluşturucular uygulamaları mümkün olmuyor.