Ders 15 / 21
Genellikler
Tip parametreleri ve kısıtlarla tek bir gövdeyi birçok tipe uygular; tembel dizinin dönüştürücüsünü her erişimde, istekli map'inkini hemen ölçer.
Yayın Güncelleme
Bu derste
- Tip parametresi ve kısıtla genel bir fonksiyon ve genel bir yapı yazmak
- İlişkili tipli bir protokolü `where` kısıtıyla bir fonksiyonda kullanmak
- Tembel `map` ile istekli `map`'in dönüştürücüyü ne zaman çağırdığını ölçmek
- Tembel bir dizinin okunmayan elemanının hiç değerlendirilmediğini göstermek
İçindekiler
Önceki derste Int için yazdığımız cubed, Double için yeniden yazılmak zorundaydı. Aynı gövdeyi bir kez yazıp birçok tipe uygulamak, genelliklerin işidir. Genellik (generics), bir fonksiyonu ya da tipi, henüz belli olmayan bir tip için yazmayı sağlar; tip, kullanım anında belli olur. Bu derste iki davranışı ölçüyoruz: genel bir gövdenin her çağrıda nasıl koştuğunu ve standart kitaplığın tembel dizilerinin dönüştürücüyü ne zaman çağırdığını.
Tip parametresi (type parameter), açılı parantez içinde yazılan yer tutucudur: <T>. Kısıt (constraint), o yer tutucunun taşıması gereken bir protokolü söyler: <T: Comparable> yalnız karşılaştırılabilen tipleri kabul eder. Kısıt olmadan gövde T hakkında hiçbir şey bilmez; < işlecini kullanamaz bile.
Ölçüm Düzeneği
Probe her derste aynı biçimde ilk bloktur. Bu derste ayrıca genel bir yapı ve ilişkili tipli bir protokol, ayrı bir yardımcı dosyada durur.
// 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)")
}
}
TS35. Her gözlem, hic dışında, iki kullanım taşır; dönüştürücüyü tek elemanlı dizilere bağlayarak eleman sayısının değerlendirme sayısını şişirmesini önlüyoruz.
// Stack.swift — genel yigit ve iliskili tipli protokol
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
}
struct Stack<Element>: Container {
private(set) var items: [Element] = []
var count: Int { items.count }
mutating func append(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
Container, bir ilişkili tip (associated type) taşır: Item. Protokol hangi tipin saklanacağını söylemez, yalnız böyle bir tipin bulunacağını söyler; uyan tip belirler. Stack<Element> ise bir genel yapıdır: Element yer tutucusu, Stack<Int> ya da Stack<String> yazıldığında gerçek bir tipe dönüşür ve Container’ın Item tipi Element olarak çıkarılır.
Genel Gövde Her Çağrıda Koşar
Aşağıdaki blok üç şeyi yan yana koyar. largest(_:tracking:), Comparable kısıtlı genel bir fonksiyondur ve iki farklı tipte çağrılır. describe, ilişkili tipe where ile kısıt koyar. Son iki gözlem aynı dönüşümün iki biçimidir: istekli map ve lazy.map.
// main.swift — genel fonksiyon, genel yapi, istekli ve tembel map
func largest<T: Comparable>(_ values: [T], tracking tracker: Probe) -> T? {
tracker.eval(values.max())
}
func describe<C: Container>(_ box: C) -> Int where C.Item: Comparable {
box.count
}
let generic = Probe("genel-fonksiyon")
let eager = Probe("istekli-map")
let lazyMap = Probe("lazy-map")
_ = generic.use { largest([3, 9, 4], tracking: generic) }
_ = generic.use { largest(["b", "a"], tracking: generic) }
var stack = Stack<Int>()
stack.append(5)
stack.append(8)
print(describe(stack))
print(stack.pop() ?? -1)
let eagerDoubled = [4].map { eager.eval($0 * 2) }
_ = eager.use { eagerDoubled[0] }
_ = eager.use { eagerDoubled[0] }
let lazyDoubled = [4].lazy.map { lazyMap.eval($0 * 2) }
_ = lazyMap.use { lazyDoubled[0] }
_ = lazyMap.use { lazyDoubled[0] }
let all = [generic, eager, lazyMap]
for probe in all { probe.report() }
tally(all)
2 8 genel-fonksiyon: 2 degerlendirme, 2 kullanim -> her-kullanimda istekli-map: 1 degerlendirme, 2 kullanim -> hemen lazy-map: 2 degerlendirme, 2 kullanim -> her-kullanimda hemen: 1 ilk-kullanimda: 0 her-kullanimda: 2 hic: 0
Genel fonksiyon her çağrıda koştu: largest iki çağrıda iki kez değerlendirildi, ve çağrılar farklı tiplerle yapıldı. Genel bir gövde tek bir yazılıştır ama her tip için ayrı bir çağrıdır; değerlendirme anı tip parametresine bağlı değildir. describe ve Stack yalnız tip düzeyinde çalıştı: where C.Item: Comparable kısıtı, Stack<Int>’in Item’ı Int olduğu için sağlandı, ve derleyici bunu çağrı anında denetledi.
Asıl ayrım son iki satırda. İstekli map, dizinin elemanlarını hemen dönüştürüp yeni bir dizi kurar; dönüştürücü eagerDoubled kurulurken bir kez koştu ve iki okuma yeniden hesaplamadı, hemen. lazy.map ise dönüştürücüyü saklayan bir tembel sarmalayıcı döndürür; dizi hiçbir şey hesaplamadan kurulur ve her erişimde dönüştürücü yeniden çağrılır. Aynı elemana iki erişim iki değerlendirme verdi: her-kullanimda. TS36. Bu sonuç dizi tabanlı tembel koleksiyon için ölçüldü; tembel bir Sequence’in yeniden dolaşılabilir olduğu varsayılmadı.
Tembel map hesaplanan özellik gibi davranır, saklanan özellik gibi değil: sonuç hiçbir yerde durmaz. Hesabı bir kez yapıp saklamak istersen dizi sarmalayıcıdan bir dizi türetirsin, ki bu istekli bir adımdır.
where Kısıtı İki Tipi Bağlar
Tek bir tip parametresine koyulan kısıt (T: Comparable) bir protokolü zorunlu kılar. where yan tümcesi ise daha fazlasını söyleyebilir: iki tip parametresinin ilişkili tiplerinin aynı olmasını ya da bir ilişkili tipin bir protokole uymasını isteyebilir. Aşağıdaki fonksiyon iki kapsayıcıyı alır ve yalnız eleman tipleri aynıysa karşılaştırır: A.Item == B.Item. Bu bir derleme zamanı denetimidir; ölçümde gövdenin değerlendirme anının değişmediğini görüyoruz.
// main.swift — where ile iki kapsayicinin eleman tipini baglamak
func sameCount<A: Container, B: Container>(_ left: A, _ right: B, tracking tracker: Probe) -> Bool
where A.Item == B.Item {
tracker.eval(left.count == right.count)
}
let whereProbe = Probe("where-kisiti")
var first = Stack<Int>()
first.append(1)
var second = Stack<Int>()
second.append(2)
second.append(3)
_ = whereProbe.use { sameCount(first, second, tracking: whereProbe) }
_ = whereProbe.use { sameCount(first, first, tracking: whereProbe) }
whereProbe.report()
where-kisiti: 2 degerlendirme, 2 kullanim -> her-kullanimda
Gövde iki çağrıda iki kez koştu: her-kullanimda. where yan tümcesi, çağrının geçerli olup olmadığını belirler; geçerli bir çağrının maliyetine ya da zamanına karışmaz. İki yığıtın eleman tipinin aynı olması burada derleyicinin çıkarımıdır ve ölçümde iz bırakmaz. Farklı eleman tipli iki yığıtın bu fonksiyona verilemeyeceğini ayrıca denemedik; kısıtın ne yaptığını gövdenin davranışından değil, imzanın okunuşundan anlıyoruz. TS37. Kısıtın reddedilmesi bu derste ölçülmedi.
Genel Yapıya Kısıtlı Uzantı
- derste
Array’e kısıtlı uzantı eklemiştik. Aynı şey kendi genel tipimiz için de geçerlidir:Stackher eleman tipiyle kullanılabilir, ama karşılaştırma isteyen bir yöntem yalnızElement: Comparableolan yığıtlarda anlamlıdır. Kısıtlı uzantı, yöntemi o durumla sınırlar. Aşağıdaki uzantı en büyük elemanı bulur.
// main.swift — Stack icin kisitli uzanti
extension Stack where Element: Comparable {
func largest(tracking tracker: Probe) -> Element? {
tracker.eval(items.max())
}
}
let constrained = Probe("kisitli-uzanti")
var numbers = Stack<Int>()
numbers.append(4)
numbers.append(9)
_ = constrained.use { numbers.largest(tracking: constrained) }
_ = constrained.use { numbers.largest(tracking: constrained) }
constrained.report()
kisitli-uzanti: 2 degerlendirme, 2 kullanim -> her-kullanimda
largest her çağrıda yığıtı yeniden tarar: her-kullanimda. Yığıtın içinde bir önbellek yok; uzantı, 07. dersteki kuralın aynısına tabidir ve saklanan bir alan ekleyemez. Genel tiplerin avantajı burada şudur: Stack<Int> için largest var, Stack<Counter> gibi karşılaştırılamayan bir eleman tipi için ise kısıtlı uzantının yöntemi sunulmaz. Genel yapı bir kez yazılır, yöntem kümesi ise eleman tipine göre değişir. TS38. Ölçüm yalnız Int elemanlı yığıtda yapıldı; Comparable olmayan elemanla yöntemin görünmediği ayrıca doğrulanmadı.
some ile Opak Dönüş Tipi
Bir genel fonksiyon, dönüş tipini çağıranın seçmesine bırakmak yerine kendisi seçebilir ve bunu gizleyebilir. Opak dönüş tipi (opaque return type), some Sequence<Int> gibi yazılır: fonksiyon belirli bir tip döndürür, ama imza yalnız “bu protokole uyan bir şey” der. Çağıran gerçek tipi bilmez, derleyici bilir. Burada soru, opak dönüşün değerlendirme anını değiştirip değiştirmediğidir. İki fonksiyon aynı dönüşümü yapar; biri lazy dizi, öteki istekli dizi döndürür, ikisi de some Sequence<Int> olarak.
// main.swift — some ile opak donus: tembel ve istekli
func lazySquares(of values: [Int], tracking tracker: Probe) -> some Sequence<Int> {
values.lazy.map { tracker.eval($0 * $0) }
}
func eagerSquares(of values: [Int], tracking tracker: Probe) -> some Sequence<Int> {
values.map { tracker.eval($0 * $0) }
}
let lazyOpaque = Probe("opak-tembel")
let eagerOpaque = Probe("opak-istekli")
let lazyResult = lazySquares(of: [3], tracking: lazyOpaque)
let eagerResult = eagerSquares(of: [3], tracking: eagerOpaque)
_ = lazyOpaque.use { Array(lazyResult) }
_ = lazyOpaque.use { Array(lazyResult) }
_ = eagerOpaque.use { Array(eagerResult) }
_ = eagerOpaque.use { Array(eagerResult) }
let all = [lazyOpaque, eagerOpaque]
for probe in all { probe.report() }
opak-tembel: 2 degerlendirme, 2 kullanim -> her-kullanimda opak-istekli: 1 degerlendirme, 2 kullanim -> hemen
İmzaları aynı olan iki fonksiyon, farklı anlar verdi. Tembel olan, her tüketimde yeniden hesapladı: her-kullanimda. İstekli olan, çağrı sırasında bir kez hesapladı ve iki tüketim bunu tekrar etmedi: hemen. some, gerçek tipi gizler ama davranışı gizlemez; dönen şeyin tembel mi istekli mi olduğu imzada yazmaz, ölçümde görünür. Bu yüzden opak dönüş tipi taşıyan bir API’de tüketimin maliyetini belgede ayrıca söylemek yararlıdır. TS39. İki fonksiyon tek elemanlı girdiyle ölçüldü; çok elemanlı girdideki değerlendirme sayısı burada kasıtlı olarak dışarıda bırakıldı.
@autoclosure Parametreli Genel Fonksiyon
Standart kitaplığın ?? işleci, sağ tarafı yalnız sol taraf nil ise değerlendirir. Bu, @autoclosure ile yapılır: parametre bir kapanışa sarılır, ama çağıran sıradan bir ifade yazar. Kendi genel fonksiyonumuzda aynı kalıbı kuruyoruz: orElse parametresi yalnız gerektiğinde çağrılır.
// main.swift — @autoclosure ile genel bir varsayilan deger
func unwrap<T>(_ value: T?, orElse backup: @autoclosure () -> T) -> T {
if let value { return value }
return backup()
}
let present = Probe("deger-var")
let absent = Probe("deger-yok")
let known: Int? = 7
let missing: Int? = nil
_ = present.use { unwrap(known, orElse: present.eval(0)) }
_ = present.use { unwrap(known, orElse: present.eval(0)) }
_ = absent.use { unwrap(missing, orElse: absent.eval(0)) }
_ = absent.use { unwrap(missing, orElse: absent.eval(0)) }
let all = [present, absent]
for probe in all { probe.report() }
tally(all)
deger-var: 0 degerlendirme, 2 kullanim -> hic deger-yok: 2 degerlendirme, 2 kullanim -> her-kullanimda hemen: 0 ilk-kullanimda: 0 her-kullanimda: 1 hic: 1
Değer varken yedek ifade hiç değerlendirilmedi: hic. Değer yokken her çağrıda değerlendirildi: her-kullanimda. Genel fonksiyon T hakkında hiçbir şey bilmez, ama kapanışın çağrılıp çağrılmayacağına kendi gövdesi karar verir; ertelenen şey tipten bağımsızdır. Bu, dersin ana temasıyla uyumludur: genel bir gövde her çağrıda koşar, ve gövdenin içindeki bir ifade bile koşmayabilir. TS40. Ölçüm Int ile yapıldı; ertelenen ifadenin yan etkisi olan tiplerde de aynı kalacağı varsayılmadı.
Genel Yapı Elemanı Saklar
Genel bir yapının içine koyduğun değer, tip parametresi ne olursa olsun, ekleme anında değerlendirilir ve yapı o değeri tutar. Stack<Int>’e append ile verilen ifade, append çağrılmadan önce çağıran tarafta hesaplanır; yığıt yalnız sonucu saklar. Bunu ölçüyoruz, çünkü genel bir kapsayıcının “tembel” ya da “hesaplanan” davranmadığını, yalnız taşıdığını görmek önemlidir.
// main.swift — genel yapi eklenen degeri saklar
let element = Probe("yigin-elemani")
var holder = Stack<Int>()
holder.append(element.eval(5))
_ = element.use { holder.count }
_ = element.use { holder.count }
_ = element.use { holder.pop() }
element.report()
yigin-elemani: 1 degerlendirme, 3 kullanim -> hemen
İfade bir kez, ekleme satırında değerlendirildi; yığıtı okumak ya da elemanı çekmek onu yeniden hesaplamadı: hemen. Genel yapı bu bakımdan bir saklanan özellik gibi davranır. Hesaplanan davranış istiyorsan, yapıya bir değer yerine bir kapanış verirsin, ve bu durumda saklanan şey kapanıştır. TS41. Tek bir ekleme ve Int ile ölçüldü; yığıtın dolup yeniden boyutlandığı durumlar bu ölçümün dışındadır.
Sınırlayıcı Ölçüm
Tembel dizinin eleman başına bir dönüştürücü çağrısı vardır, ve çağrı yalnız okunan elemana yapılır. Aşağıdaki dizi iki elemanlıdır. Dönüştürücü, ilk eleman için bir Probe’u, ikinci eleman için ötekini kullanır. Dizi yalnız first ile okunur.
// main.swift — ayni tembel dizi, hem her kullanimda hem hic
let hot = Probe("okunan-eleman")
let cold = Probe("okunmayan-eleman")
let pipeline = [10, 20].lazy.map { value -> Int in
value == 10 ? hot.eval(value + 1) : cold.eval(value + 1)
}
_ = hot.use { pipeline.first }
_ = hot.use { pipeline.first }
_ = cold.use { pipeline.first }
_ = cold.use { pipeline.first }
hot.report()
cold.report()
okunan-eleman: 4 degerlendirme, 2 kullanim -> her-kullanimda okunmayan-eleman: 0 degerlendirme, 2 kullanim -> hic
Tek bir tembel dizi iki ayrı an taşıdı. İlk eleman her first çağrısında yeniden dönüştürüldü. Blokta dört first çağrısı var ve dördü de ilk elemanı okur; hot yalnız kendi iki kullanımını saydı, ama dört değerlendirmenin hepsini gördü: her-kullanimda. İkinci elemana kimse uzanmadı, dönüştürücü ona hiç koşmadı: hic, hem de iki kullanıma karşın. Tembel dizi, istekli map‘in tersini yapar: istekli dizi tüm elemanları hemen hesaplar, tembel dizi hiçbirini önceden hesaplamaz. TS42. first yalnız ilk elemanı değerlendirdi; bu, koleksiyonun first gerçekleştirimine bağlı bir sonuçtur ve Swift 6.4’te ölçüldü.
Özet
- Genellik, bir gövdeyi tip parametresiyle bir kez yazıp birçok tipte kullanmayı sağlar; kısıt, tipin taşıması gereken protokolü söyler.
- İlişkili tip, protokolün bir tipe ihtiyaç duyduğunu ama onu uyan tipin belirleyeceğini bildirir;
whereile bu tipe kısıt konur. - İstekli
mapdönüştürücüyü hemen çağırır;lazy.mapher erişimde yeniden çağırır. - Tembel dizinin hiç okunmayan elemanı için dönüştürücü koşmaz; aynı dizi hem
her-kullanimdahemhictaşıyabilir. - Bu dersin anı: her kullanımda (
lazy.mapdönüştürücüsü, aynı elemana iki erişim).
Sonraki Adım
Şimdiye kadar her gövde sonuna kadar koştu. Peki gövdenin ortasında bir şey ters giderse, geri kalan satırlara ne olur? Sıradaki ders, hata fırlatmayı ve fırlatmadan sonraki ifadelerin değerlendirilmediğini ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.