Generátory v Javascriptu

2 JavaScript

Generátory jsou zvláštní novinkou v ES6. Na první pohled působí složitě a na druhý taky. Přinášejí však zajímavé možnosti.

Generátory v Javascriptu

Funkce s hvězdičkou

Označení generátor asi o sobě nic moc neřekne. Jde však o nový typ funkce s novou syntaxí. Klasické funkce v Javascriptu jsou od spuštění do konce svého běhu nepřerušitelné. A protože JS běží na jednom vlákně, nic jiného v tu dobu ani běžet nemůže. Co kdyby šly pozastavit a přenechaly by místo něčemu jinému? Přesně tohle generátory umí.

Funkce se označuje jako generátor hvězdičkou. Buď na konci výrazu function nebo na začátku jejího názvu. Obojí je správně.

// Z obyčejné funkce uděláme generátor hvězdičkou
// Buď
function *foo() {
  // ...
}
// Nebo
function* foo() {
  // ...
}

Takto označená funkce už se nebude chovat jako klasická. Nyní ji lze pozastavit a později znovu spustit.

Yield

Jak říct funkci, kde se má zastavit? Pomocí klíčového slovíčka yield.

function *foo() { 
  console.log('Chodí pešek okolo') 
  yield // Funkce se pozastaví 
  console.log('nedívej se na něho.') 
}

Na yield se průběh funkce zastaví a ze stejného místa bude pokračovat při dalším spuštění. To by ale samo o sobě nebylo příliš užitečné. Naštěstí toho yield umí více. Dokáže při pozastavení vrátit hodnotu a při pokračování přijmout jinou.

function *foo() {
  // Při zastavení generátor vrátí string 'Výstup'
  // Dalším spuštěním příjme hodnotu a uloží ji 
  // do proměnné "vstup"
  let vstup = yield 'Výstup'
}

Funguje to tak, že hodnota napravo od yield je při zastavení vrácena. Pokud je pak při pokračování do funkce poslána nějaká hodnota, bude dosazena na místo yieldu.

Generátor / Iterátor

Pro práci s generátory musíme vytvořit iterátor. To uděláme zavoláním funkce generátoru. Iterátor umožňuje procházet jeho jednotlivé hodnoty pomocí metody next(). Představme si, že by podobně fungovala pole. První volání next() na poli [1, 2, 3, 4] by potom vrátilo "1", druhé "2", a tak dále. Podobným způsobem můžeme iterovat generátor.

Metoda next() může přijmout jako parametr hodnotu, která bude následně dosazena do generátoru. Jak postupují jednotlivá volání, vždy je nejprve vrácena hodnota yieldu a pak dosazena nově předaná hodnota. Je to trochu, jako bychom četli jednotlivé řádky funkce zprava doleva.

function *foo() {
  let vstup = 2 * (yield 'Výstup')
  yield vstup
}

// Zavolání generátoru vrátí iterátor
const it = foo()

// První volání metody next "nastartuje" generátor
let a = it.next()
console.log(a) // '{ value: 'Výstup', done: false }'

// Druhé volání vrátí hodnotu proměnné "vstup"
// a je mu předána nová hodnota
let b = it.next(21)
console.log(b) // '{ value: 42, done: false }'

// Třetí volání dokončí generátor
let c = it.next()
console.log(c) // '{ value: undefined, done: true }'

Ukázku si můžete vyzkoušet zde.

Proč ke generátorům ještě pleteme iterátory? Objekt, který vytvoříme zavoláním generátoru totiž implementuje rozhraní Iterator (více v dokumentaci). To je také důvod, proč volání next() vrací objekt s parametry value a done, místo požadované hodnoty. První parametr obsahuje hodnotu vrácenou provedeným krokem a druhý informaci o tom, zda už je iterace generátoru dokončena.

For of

Díky implementaci tohoto rozhraní je můžeme procházet for of smyčkou. Ta do lokální proměnné automaticky přiřadí hodnotu value a skončí, jakmile bude mít done hodnotu true.

function *foo() {
  yield 'Okolo'
  yield 'Frýdku'
  yield 'ces'
  yield 'ti'
  yield 'čka!'
}

const it = foo()

// Generátory je možné procházet for of smyčkou
for (let value of it) {
  console.log(value) // 'Okolo' 'Frýdku' 'ces' 'ti' 'čka!'
}

Ukázku si můžete vyzkoušet zde.

Využití a závěr

Jaké mají vlastně generátory praktické využití? Fakt, že je možné libovolně pozastavovat funkci a předávat hodnoty dovnitř a ven, umožňuje další způsob řešení asynchronních úloh.

Mějme příklad, kdy chceme ze serveru stáhnout uživatelský příspěvek v JSON formátu a vypsat jeho nadpis. Pomocí fetch a Promise to vyřešíme snadno:

// Stáhne data příspěvku v JSON formátu
fetch('api/posts/1')
  // Zpracuje JSON na objekt
  .then(res => res.json())
  // Vypíše nadpis
  .then(post => console.log(post.title))

Na takovém řešení není nic špatně. Je přehledné a jednoduché. Co tedy teď s generátory? Řekněme, že bychom řešení do jednoho přesunuli a každý nový Promise bychom vrátili yieldem:

function *foo() {
  // Každý z obou yieldů vrátí Promise 
  // a pozastaví běh funkce
  const res = yield fetch('api/posts/1')
  const post = yield res.json()
 
  return post.title
}

Když teď generátor spustíme, narazí na první yield, pozastaví se a vrátí Promise. Co kdybychom vrácený Promise mohli vyřešit a výsledek pak předat generátoru a znova jej spustit? Jistě, narazili bychom na další yield s Promisem. Asi už víte, kde tím směřujeme:

// Funkce rekurzivně řeší každý další Promise
// vrácený yieldem, a s jeho výsledkem znovu generátor
// nastartuje, dokud není dokončen.
function run(generator) {
  const iterator = generator()
 
  function iterate(iteration) {
    // Pokud je iterace dokončena, vrátí její hodnotu
    if (iteration.done) return iteration.value
    // V opačném případě předá výsledek Promise této funkci
    const promise = iteration.value
    return promise.then( x => iterate(iterator.next(x)) )
  }
 
  return iterate(iterator.next())
}

Funkce výše vezme generátor a po každém yieldu jej znova nastartuje s výsledkem Promise, dokud není dokončen. S pomocí této funkce a generátoru lze úlohu vyřešit následovně:

run(function *() {
  const res = yield fetch('api/posts/')
  const post = yield res.json()
 
  return post.title
})
.then(title => console.log(title))

Celý příklad si můžete vyzkoušet zde.

Nyní můžeme psát asynchronní kód, který je podobný synchronnímu. Včetně, mj. podmínek a try-catch bloků. Výsledek není funkčně jiný, než třeba řešení s Promise. Ale je v některých případech přehlednější. Navíc je mnohem přirozenější psát synchronním způsobem. Pro podobná řešení existují knihovny, doporučuji třeba asynquence nebo co. Samozřejmě tento příklad není jediné využití. Další zajímavé příklady jsou např. procházení stromu, generování prvočísel nebo implementace CSP (mj. ve výše zmíněné asynquence).

Generátory mají jsou podporovány většinou nových verzí prohlížečů a NodeJS je kompletně podporuje od verze 6.4.0.

Příklad s řešením Promise jsem si vypůjčil z videa YouTube kanálu funfunfunction, který mohu vřele doporučit.


Máte zkušenosti s generátory v JS? Podělte se v komentářích

K tomuto článku již není možné přidávat další komentáře

Komentáře

Pozor, v odstavci Generátor / Iterátor v examplu je let vstup = 2 * (yield 'Haló!'). Mělo by tam být let vstup = 2 * (yield "Výstup").

Díky! Už je to opraveno :)