İçeriğe geç
academia.sh

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 sonraki yield deyimine 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; return deyiminin değeri yalnızca elle sürdürülen next çağrılarında görünür.
  • next argümanı, üretecin durduğu yield ifadesinin değeri olur; return yöntemi finally bloğ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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat