ES6: Generators

status
Published
date
Feb 9, 2019
slug
es6-generators
Draft
tags
summary
Daha önce İteratörlerden bahsetmiştim, yazı olarak da buraya birşeyler yazmıştım. Semboller ve Map-Set tiplerini çalışırken de notlar alarak çalıştığım gibi, Generatörler'de de benzer bir şey yapacağım.
type
Post
Daha önce İteratörlerden bahsetmiştim, yazı olarak da buraya birşeyler yazmıştım. Semboller ve Map-Set tiplerini çalışırken de notlar alarak çalıştığım gibi, Generatörler'de de benzer bir şey yapacağım.

İteratör neydi?

Öncelikle iteratör nedir, onu hatırlayalım.
İteratörler, yinelenebilir bir veri yapısının içinde Sembol tipiyle tanımlanmış bir fonksiyondur ve for-of döngüleri içinde bu fonksiyon kullanılır. Pratik olarak for döngülerinde sonsuz uzunluktaki diziler için kullanılabilir, diyebiliriz. Sonsuz uzunlukta bir dizi elbette olamaz, ama şu aşağıdaki kodla bu pratik olarak mümkün.
var fibonacci = {
    [Symbol.iterator] = function(){
        var pre = 0, value = 1;
        return {
            next: () => {
                [pre, value] = [value, pre + value];
                return {value, done: false};
            }
        }
    }
};

for(var val of fibonacci){
    console.log(val)
}
İteratörleri bu örnekle tekrardan hatırladık, peki generatörler nedir?

Generatörün tanımı

Generatörler, yinelenebilir fonksiyonlardır, desek çok yanlış olmaz. Aslında yinelenen, fonksiyonun kendisi değil, döndürülen değerlerdir. Generatör bir fonksiyonun tanım olarak farkı, başında bir * (yıldız) ile tanımlanır ve yineleyici içine (for-of) değer döndürmek için yield işlevi kullanılır.
Bir generatör fonksiyonu aşağıdaki gibidir.

function *get(){
    for(const x of ["a", "b", "c"]){
        yield x;
    }
}

for(var g of get()){
    console.log(g);
}

// a  b  c
Burada olan şey şu: generatör fonksiyonun içinde tanımlanan dizi, bir döngü içinde yield ile işleniyor. Burada ilk işlemde, yield işlendiğinde fonksiyon duruyor ve a değeri yield ile işlenerek fonksiyon dışında çağrılan döngü içindeki g değişkenine aktarılıyor ve ekrana basılıyor. For-of döngüsü, iteratör içindeki next fonksiyonunu tekrar çağrıyor (bunu iteraörler yazısında işlemiştik) ve get işlevindeki ikinci yield değeri dönüyor ve bu şekilde devam ediyor.
Belki bu şekilde karıştırmadan aşağıdaki gibi ele almak daha açıklayıcı olur.
function *get(){
    yield "a";
    yield "b";
    yield "c";
}

var generator = get();
generator.next(); //a
generator.next(); //b
generator.next(); //c

Değişen parametreler

yield ile fonksiyonu durdurup bir değer döndürüldüğünü gördük. Peki bu bir fonksiyonsa, durduktan sonra tekrar fonksiyona başka bir değer ulaştıramaz mıyız? yield edilen değer bir fonksiyon olsaydı, dönen cevabı yield ile atama yapabilir miydik? yield, atamaları destekliyor mu?
Destekliyor. Bunun için fonksiyona geri göndermek istediğimiz değeri .next fonksiyonuna parametre olarak vermemiz yeterli.
function *get(){
    var m1 = yield "a";
    var m2 = yield "b";
    var m3 = yield "c";
    console.log(m1, m2, m3)
}

var a = get.next();
var b = get.next(1);
var c = get.next(2);
console.log(a, b, c)
İlk .next fonksiyonuna parametre olarak bir şey vermedik. Çünkü burada gönderdiğimiz değer, yield fonksiyonu durdurduğundan dolayı atama gerçekleşmeyecekti. Ancak bir sonraki next fonksiyonu çağrıldığında bu atama gerçekleşir.
Generatör fonksiyonun sonunda yazılan console.log fonksiyonu, bir sonraki gen.next çalıştırılmayana kadar işlenmez. Çünkü fonksiyon en son yield "c" satırında duruldurulmuştur.

Asenkronlaştıralım

Javascript, senkron bir dildir, yani eşzamanlıdır. Arka arkaya iki fonksiyon çalıştırdığımız zaman, ikinci çalışan fonksiyon birincinin tamamlanmasını beklemez. Örneğin bir rest sorgusundan dönen veriyle ikinci bir fonksiyonda işlem yapacaksak, burada asenkron bir tanımlama yapmamız gerekir.
Bunu generatörlerle sağlayabiliriz! Elbette bunu yapmak için sadece generatörler yeterli olmuyor, Promise kullanmaya ihtiyacımız var. Promise, bir fonksiyon çağrıldıktan sonraki veri döndürülene kadarki adımları yöneten bir protokoldür.
Anlayabilmek için zaman ayarlı (?) bir fonksiyon tanımlayalım.
const wait = (x, t) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(x), t)
    })
}
Burada tamamlanması zaman alan bir fonksiyon tanımladık. Neden? Çünkü bize bu lazım.
wait("merhaba", 3000);
wait("güle güle", 1000);
Yukarıdaki kodun çıktısı nedir? Asenkron bir dilde, program çalıştıktan 3 saniye sonra "merhaba" değerinin, "merhaba" değeri yazıldıktan 1 saniye sonra da "güle güle" değerinin yazdırılması beklenir. Fakat Javascript senkrondur. Fonksiyonları aynı anda çalıştırır. Kuantumvâri.
Burada asenkron olarak çalıştırmak için birinci fonksiyondan sonra .then kullanımı ile bunu sağlayabiliriz.
wait("merhaba", 3000).then(res => {
    console.log("merhaba")
    wait("güle güle", 1000).then(res2 => {
        console.log(res2)
    })
});
Basitçe async/await kullanabilirdik elbette, fakat konumuz ES7 değil. ES6 sınırlarındayız şu an.
Peki bunu generatör bir fonksiyona nasıl uygulayabiliriz?
function *get(){
    yield wait("merhaba", 3000);
    yield wait("gülegüle", 100);
}
Güzel duruyor. Fakat işe yaramayacak. Çünkü generatörlerin Promise ile doğrudan konuşması mümkün değil. Burada bize dönen değerler Promise tipinde olacak. Bu şekilde bir for-of döngüsünde kullanmayı da deneyebiliriz. Hatta deneyelim.
for(var f of gen()){
    f.then(res => console.log(res));
    // güle güle
    // merhaba
}
Olmadı. Asenkron çalışması için yapmamız gereken şey, fonksiyonların birbirine bağlı olarak çalışması. Yani .then ile bağlamamız gerekiyor. Öyleyse bu generatör fonksiyonunu yöneten bir başka fonksiyon yazmamız lazım.
function PromiseGen(generator){
    const gen = generator();
    let doNext = (data) => {
        let next = gen.next(data);
        if(!next.done){
            next.value.then(doNext);
        }
    }
    doNext();
}
Bu fonksiyonla birlikte, for-of döngüsünün yaptığı işlevi taklit ettik, fakat bir farkla: next.value değerimiz bir Promise tipinde değer olduğundan, bunun içine doNext fonksiyonunu tekrar verdik ve şimdi fonksiyonları asenkron olarak çalıştırabiliriz!
Az önce yield üzerinden atama yapmak için değeri .next() ile işlememiz gerektiğini söylemiştik. Burada kullandığımız gen.next(data) tam olarak da bu işlevi görüyor.
PromiseGen(function*(){
    const m1 = yield wait("merhaba", 3000);
    console.log(m1)
    const m2 = yield wait("güle güle", 1000);
    console.log(m2)

    // merhaba
    // güle güle
})
İşte böyle 👍🏻
Peki paralel olarak çalıştırabilir miyiz? Mesela destructuring ile kullanabilsek, güzel olmaz mı?
PromiseGen(function*(){
    const [m1, m2] = yield [wait("merhaba", 3000), wait("güle güle", 1000)];
    console.log(m1, m2)
})
Burada yield edilen (yol-verilen) değer, bir array olduğu için PromiseGen fonksiyonumuzu buna göre güncellememiz gerekiyor. Çünkü orada yield edilen her değerin Promise tipinde veri döndürdüğünü varsaymıştık.
Öyleyse şöyle bir güncelleme yapmamız gerekiyor.
function PromiseGen(generator){
    const gen = generator();
    let doNext = (data) => {
        let next = gen.next(data);
        if(Array.isArray(next.value)){
            Promise.all(next.value).then(doNext);
        }
        if(!next.done && next.value.then){
            next.value.then(doNext);
        }
    }
    doNext();
}
Hadi deneyelim!
PromiseGen(function*(){
    const [m1, m2] = yield [wait("merhaba", 3000), wait("güle güle", 1000)];
    console.log(m1, m2)
    // merhaba güle güle
})
Şiir gibi 💃🏻

Sonuç

Generatörlere günlük kullanımda pek rastlamayız. Yazıda göstermiş olduğum asenkron fonksiyon oluşturmak için kullanılabilir, fakat güncel olarak ES7 ile gelen async/await yapısıyla bunu çok daha kolay bir şekilde sağlayabiliyoruz. Generatörler farklı amaçlarla da elbette kullanılabilir, fakat araştırdığım kadarıyla ve kurmaya çalıştığım yapılarla günlük kullanım için en iyi örneğin bu olduğuna karar verdim ve bunun için yazıda bu örneği kullandım.
Burada anlattıklarımı video kaynak olarak oluşturdum. Şuradan erişebilirsiniz.

Kaynaklar


© Samet 2017 - 2024