Ders 15 / 17
Üreteçler
Duraklatılabilir fonksiyonlar, üreteç nesnesinin yineleme protokolündeki yeri, sonu olmayan diziler, yetki devri, iki yönlü iletişim ve tembel boru hatları.
İçindekiler
Önceki derste elle yazılan yineleyicilerde aynı üç parça sürekli tekrarlandı: konumu tutan
değişken, bitiş koşulu ve kendini döndüren [Symbol.iterator] yöntemi. Üçü de mekaniktir
ve fonksiyonun asıl işini gölgeler.
Üreteç (generator), yürütmesini durdurup daha sonra kaldığı yerden sürdürebilen bir
fonksiyondur. function* biçiminde tanımlanır ve yield deyimiyle bir değer verip durur.
Çağrıldığında gövdesi çalışmaz; protokolün iki sözleşmesini de karşılayan bir nesne
döndürür. Ders bu nesnenin davranışını açar ve önceki dersteki tembel seriyi birkaç
satıra indirir.
Duraklatılabilir Fonksiyon
Üreteç fonksiyonun gövdesi, ilk next çağrısına kadar hiç çalışmaz. Her next, bir
sonraki yield deyimine kadar ilerler ve orada durur.
let hesapSayisi = 0; function* kalibreliSeri(hamDegerler, kayma) { for (const ham of hamDegerler) { hesapSayisi += 1; yield Number((ham + kayma).toFixed(2)); } } const uretec = kalibreliSeri([21.4, 19.8, 25.1], -0.4); console.log(hesapSayisi); console.log(typeof uretec.next); console.log(uretec[Symbol.iterator]() === uretec); console.log(JSON.stringify(uretec.next())); console.log(hesapSayisi); console.log(JSON.stringify(uretec.next())); console.log(JSON.stringify(uretec.next())); console.log(JSON.stringify(uretec.next())); console.log(hesapSayisi); console.log([...kalibreliSeri([21.4, 19.8], -0.4)].join(",")); console.log(typeof kalibreliSeri);
0
function
true
{"value":21,"done":false}
1
{"value":19.4,"done":false}
{"value":24.7,"done":false}
{"done":true}
3
21,19.4
function
İlk satır gövdenin hiç çalışmadığını gösterir. Üçüncü satır üreteç nesnesinin hem
yineleyici hem gezilebilir olduğunu doğrular: kendisini döndüren bir [Symbol.iterator]
yöntemi taşır. Önceki derste elle yazılan üç parçanın hepsi burada dilin kendisi
tarafından üretilmiştir.
Beşinci satır tembelliği ölçer: ilk next yalnızca bir hesap yapmıştır. Değerler bittiğinde
done alanı doğru olur ve value alanı undefined kalır. Son satır bir ayrımı sabitler —
üreteç fonksiyonun kendisi sıradan bir fonksiyondur; üreteç olan, çağrının döndürdüğü
nesnedir.
Üreteç nesnesi de tükenir. Yeniden gezmek için fonksiyonun tekrar çağrılması gerekir; onuncu satırdaki yayma bunun için yeni bir üreteç kurar.
Sınıf Yöntemi Olarak Üreteç
Yöntem adının önüne yıldız konarak sınıf gövdesinde de üreteç tanımlanabilir. Önceki
dersteki OlcumSerisi sınıfı böylece yarı yarıya kısalır.
class OlcumSerisi { constructor(sensor, degerler, kayma = 0) { this.sensor = sensor; this.degerler = degerler; this.kayma = kayma; } *[Symbol.iterator]() { for (const ham of this.degerler) { yield Number((ham + this.kayma).toFixed(2)); } } *etiketliCiftler() { let sira = 0; for (const deger of this) { yield [`${this.sensor}#${sira}`, deger]; sira += 1; } } } const seri = new OlcumSerisi("S-01", [21.4, 19.8, 25.1], -0.4); console.log([...seri].join(",")); console.log([...seri].join(",")); for (const [etiket, deger] of seri.etiketliCiftler()) { console.log(`${etiket} -> ${deger}`); } console.log(Object.getOwnPropertyNames(OlcumSerisi.prototype).join(",")); console.log(Object.hasOwn(OlcumSerisi.prototype, Symbol.iterator)); console.log(new Map(seri.etiketliCiftler()).get("S-01#1"));
21,19.4,24.7 21,19.4,24.7 S-01#0 -> 21 S-01#1 -> 19.4 S-01#2 -> 24.7 constructor,etiketliCiftler true 19.4
Seri iki kez gezilebilir, çünkü gezilen şey serinin kendisidir ve her gezinme yeni bir
üreteç nesnesi üretir. etiketliCiftler yöntemi ise for (const deger of this) yazarak
kendi sınıfının gezilebilirliğinden yararlanır; ham veriye ikinci kez erişmez.
Yedinci ve sekizinci satırlar zincirle bağı yineler: üreteç yöntemleri de prototipte
durur ve simge anahtarlı olan Object.keys çıktısına girmez. Son satır ise protokolün
birleştirici gücünü gösterir — anahtar–değer çiftleri veren herhangi bir gezilebilir
nesne doğrudan Map yapıcısına verilebilir.
Sonu Olmayan Diziler ve Yetki Devri
Üreteç yalnızca istendiğinde çalıştığı için, sonu olmayan bir dizi tanımlamak
sorunsuzdur. yield* ise bir üretecin işi başka bir gezilebilir nesneye devretmesini
sağlar.
function* artanZaman(baslangic, adim) { let zaman = baslangic; while (true) { yield zaman; zaman += adim; } } function* ilkN(gezilebilir, adet) { if (adet <= 0) return; let kalan = adet; for (const deger of gezilebilir) { yield deger; kalan -= 1; if (kalan <= 0) return; } } console.log([...ilkN(artanZaman(1000, 60), 4)].join(",")); function* onSeri() { yield 21.4; yield 19.8; } function* arkaSeri() { yield 25.1; } function* tumSeri() { yield* onSeri(); yield* arkaSeri(); yield* [18.2, 30.0]; return "bitti"; } const birlesik = tumSeri(); console.log([...tumSeri()].join(",")); let adim = birlesik.next(); const degerler = []; while (!adim.done) { degerler.push(adim.value); adim = birlesik.next(); } console.log(degerler.length); console.log(adim.value);
1000,1060,1120,1180 21.4,19.8,25.1,18.2,30 5 bitti
artanZaman sonsuz bir döngü içerir ama program donmaz: değerler yalnızca istendiğinde
üretilir ve ilkN dördüncü değerden sonra durur. Sonsuz döngünün tehlikesiz olması
tembelliğin doğrudan sonucudur; aynı ölçüm zamanlarını bir dizide üretmek imkânsızdır.
yield* her tür gezilebilir nesneyi kabul eder — başka bir üreteç ya da sıradan bir dizi.
return deyiminin değeri ise for...of ve yayma tarafından atlanır; yalnızca next
çağrısını elle sürdüren kod, bitiş adımının value alanında görebilir. Bu yüzden üreteçler
sonuç döndürmek için değil, değer akıtmak için kullanılır.
İki Yönlü İletişim
next çağrısına verilen argüman, üretecin durduğu yield ifadesinin değeri olur. Böylece
üreteç yalnızca değer üretmez, dışarıdan değer de alır.
function* esikDenetleyici(baslangicEsigi) { let esik = baslangicEsigi; const asanlar = []; try { while (true) { const gelen = yield `esik=${esik}, asan=${asanlar.length}`; if (typeof gelen === "number") { if (gelen > esik) asanlar.push(gelen); } else if (typeof gelen === "object" && gelen !== null) { esik = gelen.yeniEsik; } } } finally { console.log(`kapanis: toplam asan=${asanlar.length}`); } } const denetleyici = esikDenetleyici(20); console.log(denetleyici.next("bu deger yok sayilir").value); console.log(denetleyici.next(21.4).value); console.log(denetleyici.next(19.8).value); console.log(denetleyici.next({ yeniEsik: 25 }).value); console.log(denetleyici.next(25.1).value); console.log(JSON.stringify(denetleyici.return("elle durduruldu"))); console.log(JSON.stringify(denetleyici.next(30)));
esik=20, asan=0
esik=20, asan=1
esik=20, asan=1
esik=25, asan=1
esik=25, asan=2
kapanis: toplam asan=2
{"value":"elle durduruldu","done":true}
{"done":true}
İlk next çağrısının argümanı yok sayılır: gövde henüz hiçbir yield ifadesinde durmuş
değildir, dolayısıyla değeri alacak bir yer yoktur. Sonraki çağrılarda gönderilen değer
gelen değişkenine düşer.
return yöntemi üreteci dışarıdan sonlandırır ve finally bloğunu çalıştırır — önceki
dersteki temizlik noktasının üreteçlerdeki karşılığı budur. Sonlanmış bir üreteç sonraki
next çağrılarında bitmiş olarak yanıt verir. throw yöntemi de benzer biçimde, hatayı
üretecin durduğu noktaya yükseltir.
Kapanışlar dersindeki sayaç ile karşılaştırıldığında fark açıktır: kapanış durumu tutar ama yürütmeyi tutmaz. Üreteç, durumun yanında programın nerede kaldığını da saklar.
Tembel Boru Hattı
Üreteçlerin en somut kazancı, dönüşüm zincirlerinin ara diziler üretmeden kurulmasıdır. Süzme ve eşleme birer üreteç olarak yazıldığında, her değer boru hattının tamamından tek tek geçer.
let okumaSayisi = 0; let donusumSayisi = 0; function* hamKaynak(degerler) { for (const deger of degerler) { okumaSayisi += 1; yield deger; } } function* suz(gezilebilir, olcut) { for (const deger of gezilebilir) { if (olcut(deger)) yield deger; } } function* esle(gezilebilir, donusum) { for (const deger of gezilebilir) { donusumSayisi += 1; yield donusum(deger); } } function* ilkN(gezilebilir, adet) { if (adet <= 0) return; let kalan = adet; for (const deger of gezilebilir) { yield deger; kalan -= 1; if (kalan <= 0) return; } } const ham = [21.4, 19.8, 25.1, 18.2, 30.0, 27.3, 15.0]; const boruHatti = ilkN( esle( suz(hamKaynak(ham), (d) => d > 20), (d) => Number((d * 1.8 + 32).toFixed(1)), ), 2, ); console.log(okumaSayisi); console.log([...boruHatti].join(",")); console.log(okumaSayisi); console.log(donusumSayisi); const dizilerle = ham .filter((d) => d > 20) .map((d) => Number((d * 1.8 + 32).toFixed(1))) .slice(0, 2); console.log(dizilerle.join(","));
0 70.5,77.2 3 2 70.5,77.2
İki yol aynı sonucu verir; yaptıkları iş farklıdır. Üreteçli boru hattı yedi ham değerden yalnızca üçünü okur ve iki dönüşüm yapar; gerekli olan iki sonuç elde edildiği anda durur. Dizi yöntemleriyle yazılan sürüm önce beş değeri süzer, beşini de dönüştürür, sonra ilk ikisini alır — üstelik iki ara dizi üretir.
Fark, veri kaynağı büyüdükçe ya da dönüşüm pahalılaştıkça belirginleşir. Buna karşılık üreteçli kuruluşun sabit bir maliyeti vardır: her değer için birkaç yürütme geçişi olur. Küçük dizilerde dizi yöntemleri hem daha kısa hem daha hızlıdır; ölçüt, işlenmeyen elemanların sayısıdır.
Özet
- Üreteç fonksiyon çağrıldığında gövdesi çalışmaz; hem yineleyici hem gezilebilir olan bir üreteç nesnesi döner.
- Her
nextçağrısı bir sonrakiyielddeyimine kadar ilerler; yürütme orada durur ve durum korunur. - Sınıf gövdesinde yıldızlı yöntem tanımlanabilir; üreteç yöntemleri de prototipte durur.
yield*işi başka bir gezilebilir nesneye devreder;returndeyiminin değeri yalnızca elle sürdürülennextçağrılarında görünür.nextargümanı, üretecin durduğuyieldifadesinin değeri olur;returnyöntemifinallybloğunu çalıştırarak temizlik yapar.- Üreteçlerden kurulu boru hattı ara dizi üretmez ve yalnızca istenen kadar değer işler.
Sonraki Adım
Bu konudaki koleksiyonların hepsi keyfi JavaScript değerleri tuttu: sayı, dizgi, nesne. Bir ölçüm akışını dosyaya yazmak ya da ağ üzerinden aktarmak gerektiğinde ise veri, bayt dizisi olarak temsil edilmelidir. Sonraki ders sabit boyutlu ikili arabellekleri ve üzerlerindeki tipli görünümleri ele alacak; bayt sırası ve taşma davranışı da orada sınanacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.