Nejprve ještě nutná poznámka. Uvedené příklady jsou funkční v node.js od verze 0.11.x s použitím přepínače –harmony-generators:
$ node --harmony-generators
V Google Chrome si podporu můžete zapnout na stránce s nastavením experimentálních voleb chrome://flags/#
.
Co jsou ty generátory zač?
Vlastně jsou to koprogramy. V ECMA6 jsou zastoupeny objektem zapouzdřujícím uspaný kontext běhu programu. Možná to zní nesrozumitelně, pojďme si raději ukázat příklad:
function* helloGenerator() {
yield "Ahoj";
yield "generátore!";
}
var hello = helloGenerator();
console.log(hello.next()); // { value: 'Ahoj', done: false }
console.log(hello.next()); // { value: 'generátore!', done: false }
console.log(hello.next()); // { value: undefined, done: true }
// console.log(hello.next()); // throws Error: Generator has already finished
Takto se například nechá definovat Fibonacciho posloupnost:
function* fibonacciGenerator() {
var fib, prev = 0, curr = 1;
while(true) {
fib = prev + curr;
prev = curr;
yield curr = fib;
}
}
var fibonacci = fibonacciGenerator();
console.log(fibonacci.next()); // { value: 1, done: false }
console.log(fibonacci.next()); // { value: 2, done: false }
console.log(fibonacci.next()); // { value: 3, done: false }
console.log(fibonacci.next()); // { value: 5, done: false }
console.log(fibonacci.next()); // { value: 8, done: false }
Generátoru lze předat zpět hodnotu zavoláním next(hodnota)
. Toto je velice důležitá vlastnost, kterou dále v článku použijeme. Zde je příklad:
function* zvedavyGenerator() {
var result = yield 'Kdo jsem?';
return result === 'Generátor' ? 'Ano! :-)' : 'Ne!';
}
var otazky = zvedavyGenerator();
// Předávat hodnotu prvnímu volání next() nemá smysl
console.log(otazky.next()); // { value: 'Kdo jsem?', done: false }
console.log(otazky.next("Generátor")); // { value: 'Ano! :-)', done: true }
Ještě si ukážeme, jak vyvolat výjimku uvnitř generátoru, to se bude také hodit:
function* zlobivyGenerator() {
var i = 0;
try {
while (true) yield i++;
} catch (err) {
console.log('Ouha ' + err);
while (i--) yield i;
}
}
var generator = zlobivyGenerator();
console.log(generator.next()); // { value: 0, done: false }
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
generator.throw(new Error("Chyba")); // Ouha Error: Chyba
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 0, done: false }
console.log(generator.next()); // { value: undefined, done: true }
Generátory lze též skládat pomocí syntaxe yield*
(všimněte si hvezdičky)
function* sequenceGenerator(start, increment, max) {
while (start < max) {
yield start;
start += increment;
}
}
function* composedGenerator() {
// yield s hvezdičkou vrací celý generátor
yield* sequenceGenerator(0, 2, 10);
yield* sequenceGenerator(10, 20, 100);
yield* sequenceGenerator(100, 200, 1000);
}
var sequence = composedGenerator();
while (true) {
var item = sequence.next();
if (item.done) return;
console.log(item.value);
}
// vypíše 0 2 4 6 8 10 30 50 70 90 100 300 500 700 900
K čemu je to dobré?
Tak, schválně, nikoho nic nenapadá? :-) Tak jdem na to:
var fs = require('fs');
// toto je náš aplikační kód
function *taskGenerator() {
var lines = [];
for (var i = 1; i <= 6; i++) {
var filename = './generator' + i + '.js';
lines.push('File: ' + filename);
lines.push(yield readFileThunk(filename, { encoding: 'utf-8' }));
lines.push('========================');
}
console.log(lines.join('\n'));
}
// co je "thunk" si popíšeme dále v textu
function readFileThunk(filename, options) {
return function(callback) {
fs.readFile(filename, options, callback);
}
}
// toto teď moc nestudujte, není to až tak třeba
// popíšeme si lepší způsob, jak docílit téhož
// daleko lepší je použít modul co
function procesTasks(tasks, callback, value) {
var task = tasks.next(value);
if (typeof task.value === 'function') {
task.value(function(err, result) {
procesTasks(tasks, callback, result);
});
} else {
if (typeof callback === 'function') {
callback(new Error('Unsupported return value'));
}
}
}
// a teprve teď to celé zpracujeme
procesTasks(taskGenerator());
Všimněte si kódu v taskGenerator
. Vidíte, jak je krásně čitelný? (No dobře, ještě to tak pěkné není, ale bude to hezčí :-) ) Přitom v sekvenci zpracovává 6 asynchronních úkolů! Jak je to možné? Klíčem zde je takzvaný thunk, který se z generátoru navrací jakémusi plánovači, v našem příkladu je reprezentován poněkud naivní rekurzivní funkcí procesTasks
. V praxi existují připravené npm moduly.
Co je to Thunk
Thunk se dá chápat jako úmysl. Je to funkce, která vrací funkci přijímající jediný callback (tedy další funkci), který přijímá dva parametry (err, result)
. Všimněte si funkce readFileThunk
v příkladu výše. Je to vlastně jakási low-level variace na promisy vyhovující konvenčnímu tvaru callbacků v node.js core.
Zjednodušeně řečeno, thunk umožňuje převést kód z tohoto:
fs.readFile(filename, options, callback);
na tento:
readFileThunk(filename, options)(callback);
Existuje i modul thunkify, který toto umí udělat s jakoukoliv funkcí, která přijímá callback jako poslední parametr:
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
Ještě jeden příklad jednoduchého, ale užitečného thunku, který pozastaví aktuální generátor, aby mohly pokračovat jiné asynchronní úkoly. Nesmíme nikdy zapomínat, že samotný náš JavaScriptový kód běží pouze v jednom vlákně a asynchronní úkoly pouze informují náš kód, že byly dokončeny, když náš kód čeká.
function immediateThunk() {
return function(callback) {
setImmediate(function() {
callback();
});
}
}
Thunky se mi nelíbí :-( Něco jiného? CO?
Není problém :-) Naštěstí TJ Holowaychuck napsal modul co, který funguje stejně jako naše metoda procesTasks
, s tím rozdílem, že lze pomocí yield
vynášet nejen thunky, ale i promisy a jejich pole a objekty, generátory a generátorové funkce. Modul je krásně zdokumentován.
Pro ilustraci si přepíšeme předchozí příklad vypisující zdrojové kódy příkladů:
var fs = require('fs');
var co = require('co');
var thunkify = require('thunkify');
var read = thunkify(fs.readFile);
// toto je náš aplikační kód
co(function *() {
var lines = [];
for (var i = 1; i <= 6; i++) {
var filename = './generator' + i + '.js';
lines.push('File: ' + filename);
lines.push(yield read(filename, { encoding: 'utf-8' }));
lines.push('========================');
}
console.log(lines.join('\n'));
})();
Není to hned nějaké kratší? :-) Navíc sama funkce co
vrací thunk, lze tedy napsat:
co(function *() {
return (yield read('./generator1.js', { encoding: 'utf-8' })) +
(yield read('./generator2.js', { encoding: 'utf-8' }));
})(function(err, result) {
console.log(result);
});
A CO a chyby?
Výjimky fungují tak, jak by měly, konečně :-)
co(function *() {
try {
return (yield read('./generator1.js', { encoding: 'utf-8' })) +
(yield read('./generator2.js', { encoding: 'utf-8' })) +
(yield read('neexistujici-soubor', { encoding: 'utf-8' }));
} catch (err) {
throw new Error('Něco je špatně');
}
})(function(err, result) {
if (err) {
console.error('Chyba: ' + err.message);
// Chyba: Něco je špatně: ENOENT, open 'neexistujici-soubor'
return;
}
console.log(result);
});
Paralelní zpracování
Spustit více asynchronních operací paralelně je s co
také velice snadné:
co(function *() {
var results = yield [
read('./co1.js', { encoding: 'utf8'}),
read('./co2.js', { encoding: 'utf8'}),
read('./co3.js', { encoding: 'utf8'})];
console.log(results.join('\n===\n'));
})();
Alternativně lze předat objekt, což je dobré pro pojmenované úlohy:
co(function *() {
var results = yield {
'co1': read('./co1.js', { encoding: 'utf8'}),
'co2': read('./co2.js', { encoding: 'utf8'}),
'co3': read('./co3.js', { encoding: 'utf8'})}
console.log(results);
})();
Dokonce lze objekty a pole různě zanořovat a kombinovat yieldy:
co(function *() {
var results = yield {
'jmeno': print('jmeno'),
'prijmeni': print('prijmeni'),
'id': yield print('id'),
'konstanta5': 5,
'konstantaStr': 'konstantaStr',
'pole': [
print('pole[0]'),
print('pole[1]'),
[
print('pole[2][0]'),
[
print('pole[2][1][0]'),
print('pole[2][1][1]'),
],
{
'pozdni': print('pole[2][2].pozdni'),
'drivejsi': yield print('pole[2][2].drivejsi')
},
print('pole[2][3]'),
],
print('pole[3]'),
],
};
console.log(JSON.stringify(results, null, 2));
})();
function print(name) {
return function(callback) {
console.log('Zpracovávám ' + name);
setTimeout(function() {
callback(null, 'hodnota ' + name);
}, 500 + Math.random() * 1000);
};
}
// Zpracovávám id
// ... čeká
// Zpracovávám pole[2][2].drivejsi
// ... čeká
// Zpracovávám jmeno
// Zpracovávám prijmeni
// Zpracovávám pole[0]
// Zpracovávám pole[1]
// Zpracovávám pole[2][0]
// Zpracovávám pole[2][1][0]
// Zpracovávám pole[2][1][1]
// Zpracovávám pole[2][2].pozdni
// Zpracovávám pole[2][3]
// Zpracovávám pole[3]
// ... čeká
({
"id": "hodnota id",
"konstanta5": 5,
"konstantaStr": "konstantaStr",
"prijmeni": "hodnota prijmeni",
"jmeno": "hodnota jmeno",
"pole": [
"hodnota pole[0]",
"hodnota pole[1]",
[
"hodnota pole[2][0]",
[
"hodnota pole[2][1][0]",
"hodnota pole[2][1][1]"
],
{
"drivejsi": "hodnota pole[2][2].drivejsi",
"pozdni": "hodnota pole[2][2].pozdni"
},
"hodnota pole[2][3]"
],
"hodnota pole[3]"
]
});
Všechny příklady zde uvedené jsou na GitHubu
Příště…
Příště se podíváme na serverový framework koa od tvůrců Express, který staví na tomto přístupu.
Přehled komentářů