Apr 23, 2018 – 10 min čtení

ES6 zavedl nový způsob práce s funkcemi a iterátory v podobě generátorů (nebo generátorových funkcí). Generátor je funkce, která se může zastavit v polovině cesty a pak pokračovat od místa, kde se zastavila. Stručně řečeno, generátor se tváří jako funkce, ale chová se jako iterátor.

Fun Fact: async/await může být založen na generátorech. Více se dočtete zde.

Generátory jsou s iterátory úzce propojeny. Pokud iterátory neznáte, zde je článek, který vám pomůže je lépe pochopit.

Tady je jednoduchá analogie, abyste měli intuici pro generátory, než přejdeme k technickým detailům.

Představte si, že čtete techno-thriller, ve kterém se koušou nehty. Celí pohroužení do stránek knihy sotva zaslechnete zvonek u dveří. Je to poslíček s pizzou. Vstanete, abyste otevřeli dveře. Než to však uděláte, nastavíte si záložku na poslední přečtenou stránku. V duchu si uložíte události děje. Pak si jdete pro pizzu. Jakmile se vrátíte do svého pokoje, začnete knihu číst od stránky, na kterou jste si dali záložku. Nezačínáte ji znovu od první stránky. V jistém smyslu jste se chovali jako generátor funkce.

Podívejme se, jak můžeme využít generátory k řešení některých běžných problémů při programování. Ještě předtím si však definujme, co to generátory jsou.

Co jsou generátory?

Obyčejnou funkci, jako je tato, nelze zastavit dříve, než dokončí svůj úkol, tj. provede se její poslední řádek. Řídí se něčím, čemu se říká model run-to-completion.

function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}

Jediný způsob, jak ukončit normalFuncfunkci, je returnz ní vystoupit nebo throwvyvolat chybu. Pokud funkci zavoláte znovu, začne se provádět znovu od začátku.

Naproti tomu generátor je funkce, která se může zastavit v polovině a pak pokračovat od místa, kde se zastavila.

Zde jsou uvedeny další běžné definice generátorů –

  • Generátory jsou speciální třídou funkcí, které zjednodušují úlohu psaní iterátorů.
  • Generátor je funkce, která místo jedné hodnoty vytváří posloupnost výsledků, tj. generujete řadu hodnot.

V jazyce JavaScript je generátor funkce, která vrací objekt, na který můžete zavolat next(). Každé volání funkce next() vrátí objekt tvaru –

{ 
value: Any,
done: true|false
}

Vlastnost value bude obsahovat hodnotu. Vlastnost done je buď true, nebo false. Jakmile se done stane true, generátor se zastaví a nebude generovat žádné další hodnoty.

Tady je ilustrace téhož –

Normální funkce vs. generátory

Všimněte si přerušované šipky, která uzavírá smyčku yield-resume-yield těsně před Finish v části obrázku Generátory. Existuje možnost, že generátor nikdy neskončí. Příklad uvidíme později.

Vytvoření generátoru

Podívejme se, jak můžeme vytvořit generátor v jazyce JavaScript –

function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined

Soustřeďte se na tučné části. Pro vytvoření funkce generátoru použijeme syntaxi function * místo pouhého function. Mezi klíčovým slovem function, * a názvem funkce může být libovolný počet mezer. Protože se jedná pouze o funkci, můžeme ji použít kdekoli, kde lze použít funkci, tj. uvnitř objektů a metod tříd.

Uvnitř těla funkce nemáme return. Místo toho máme další klíčové slovo yield (řádek 2). Je to operátor, pomocí kterého se generátor může pozastavit. Pokaždé, když generátor narazí na yield, „vrátí“ hodnotu uvedenou za ním. V tomto případě je vrácena hodnota Hello,. V kontextu generátorů však neříkáme „vrátil“. Říkáme „generátor vrátil Hello, „.

Z generátoru můžeme také vracet. Avšak return nastaví vlastnost done na hodnotu true, po které už generátor nemůže generovat žádné další hodnoty.

function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}

V řádku 3 vytvoříme objekt generátoru. Vypadá to, jako bychom volali funkci generatorFunction. Skutečně tomu tak je! Rozdíl je v tom, že funkce generátoru místo toho, aby vracela nějakou hodnotu, vždy vrací objekt generátoru. Objekt generátoru je iterátor. Můžete jej tedy použít ve smyčkách for-of nebo v jiných funkcích přijímajících iterovatelný objekt.

V řádku 4 voláme metodu next() na objektu generatorObject. Tímto voláním se začne vykonávat generátor. Nejprve console.log provede This will be executed first. Poté se setká s yield 'Hello, '. Generátor vydá hodnotu jako objekt { value: 'Hello, ', done: false } a pozastaví se/pozastaví se. Nyní čeká na další volání.

Na pátém řádku opět voláme next(). Tentokrát se generátor probudí a začne vykonávat od místa, kde skončil. Další řádek, který najde, je console.log. Zaznamená řetězec I will be printed after the pause. Poté narazí na další řádek yield. Hodnota je vydána jako objekt { value: 'World!', done: false }. Vyjmeme vlastnost value a zaznamenáme ji. Generátor se opět uspí.

V řádku 6 opět vyvoláme next(). Tentokrát již není třeba provádět žádné další řádky. Nezapomeňte, že každá funkce implicitně vrací undefined, pokud není uveden příkaz return. Proto generátor vrací (místo aby poskytl) objekt { value: undefined, done: true}. Objekt done je nastaven na true. To signalizuje konec tohoto generátoru. Nyní nemůže generovat další hodnoty ani znovu pokračovat, protože již nejsou žádné příkazy, které by bylo třeba provést.

Pro opětovné spuštění generátoru budeme muset vytvořit nový jiný objekt generátoru.

Použití generátorů

Existuje mnoho úžasných případů použití generátorů. Podívejme se na několik z nich.

Implementace iterátorů

Když implementujete iterátor, musíte ručně vytvořit objekt iterátoru s metodou next(). Také musíte ručně uložit jeho stav. Často se stává, že je to opravdu obtížné. Protože generátory jsou také iterátory, lze je použít k implementaci iterátorů bez dalšího kotelního kódu. Podívejme se na jednoduchý příklad:

Problém: Chceme vytvořit vlastní iterovatelnou tabulku, která vrací This, is a iterable.. Zde je jedna implementace pomocí iterátorů –

const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.

Tady je totéž pomocí generátorů –

function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.

Můžete porovnat obě verze. Je pravda, že se jedná o poněkud vykonstruovaný příklad. Ale ilustruje to –

  • Nemusíme se starat o Symbol.iterator
  • Nemusíme implementovat next().
  • Nemusíme ručně vytvářet návratový objekt next() tj. { value: 'This', done: false }.
  • Nemusíme ukládat stav. V příkladu iterátoru byl stav uložen do proměnné step. Její hodnota definovala to, co bylo vypsáno z iterátoru. V generátoru jsme nic takového dělat nemuseli.

Lepší asynchronní funkce

Kód využívající sliby a zpětná volání, jako je –

function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}

, lze zapsat jako (s pomocí knihoven, jako je co.js)-

const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});

Někteří čtenáři si možná všimli, že je to paralelní s použitím async/await. To není náhoda. async/await se může řídit podobnou strategií a v případech, kdy se jedná o sliby, nahrazuje výnos await. Může být založen na generátorech. Více informací najdete v tomto komentáři.

Konečné datové proudy

Je možné vytvořit generátory, které nikdy neskončí. Uvažujme tento příklad –

function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2

Vytvoříme generátor naturalNumbers. Uvnitř této funkce máme nekonečnou smyčku while. V této smyčce yield vytvoříme num. Když generátor vynáší, pozastaví se. Když znovu zavoláme next(), generátor se probudí, pokračuje od místa, kde byl pozastaven (v tomto případě yield num), a vykonává se, dokud nenarazíme na další yield nebo dokud generátor neskončí. Protože dalším příkazem je num = num + 1, aktualizuje se num. Poté přejde na začátek cyklu while. Podmínka je stále pravdivá. Narazí na další řádek yield num. Vydá aktualizovaný num a pozastaví se. To pokračuje tak dlouho, jak chcete.

Generátory jako pozorovatelé

Generátory mohou také přijímat hodnoty pomocí funkce next(val). Pak se generátor nazývá pozorovatel, protože se probudí, když obdrží nové hodnoty. V jistém smyslu stále pozoruje hodnoty a jedná, když nějakou získá. Více o tomto vzoru si můžete přečíst zde:

Výhody generátorů

Jak je vidět na příkladu s nekonečnými datovými proudy, je to možné jen díky línému vyhodnocování. Lazy Evaluation je vyhodnocovací model, který odkládá vyhodnocení výrazu, dokud není potřeba jeho hodnota. To znamená, že pokud hodnotu nepotřebujeme, nebude existovat. Vypočítá se tak, jak ji požadujeme. Podívejme se na příklad –

function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}

Příklad powerSeries udává řadu čísel zvýšených na mocninu. Například mocninná řada čísla 3 zvýšeného na 2 by byla 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Když provedeme const powersOf2 = powerSeries(3, 2);, vytvoříme pouze objekt generátoru. Žádná z hodnot nebyla vypočtena. Kdybychom nyní zavolali next(), bylo by vypočteno a znovu naladěno 9.

Paměťově úsporné

Přímým důsledkem líného vyhodnocování je, že generátory jsou paměťově úsporné. Generujeme pouze ty hodnoty, které jsou potřeba. U běžných funkcí jsme museli všechny hodnoty předem vygenerovat a uchovávat je pro případ, že bychom je později použili. S generátory však můžeme výpočet odložit, dokud ho nebudeme potřebovat.

Můžeme vytvořit kombinátorové funkce, které budou působit na generátory. Kombinátory jsou funkce, které kombinují existující iterace a vytvářejí nové. jedním z takových kombinátorů je take. Bere první n prvky iterovatelné tabulky. Zde je jedna implementace –

function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}

Tady je několik zajímavých případů použití take

take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49

Tady je implementace cyklické knihovny (bez funkce reverzace).

function * cycled(iter) {
const arrOfValues =
while (true) {
for (const val of arrOfValues) {
yield val
}
}
}console.log(...take(10, cycled(take(3, naturalNumbers()))))// 1 2 3 1 2 3 1 2 3 1

Kaverzace

Při programování pomocí generátorů byste měli mít na paměti několik bodů.

  • Objekty generátorů mají pouze jednorázový přístup. Jakmile vyčerpáte všechny hodnoty, nemůžete je znovu procházet. Chcete-li hodnoty vygenerovat znovu, musíte vytvořit nový objekt generátoru.
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
  • Objekty generátoru neumožňují náhodný přístup, jak je to možné u polí. Vzhledem k tomu, že hodnoty jsou generovány postupně, přístup k náhodné hodnotě by vedl k výpočtu hodnot do daného prvku. Proto se nejedná o náhodný přístup.

Závěr

V generátorech je ještě mnoho věcí, kterými se musíme zabývat. Věci jako yield *, return() a throw(). Generátory také umožňují koroutiny. Uvedl jsem několik odkazů, které si můžete přečíst, abyste generátorům lépe porozuměli.

Můžete zamířit na stránku itertools Pythonu a prohlédnout si některé utility, které umožňují práci s iterátory a generátory. Jako cvičení můžete tyto utility sami implementovat.

admin

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.

lg