Souběžné s generátory ES6

Generátory ES6:Kompletní série

  1. Základy generátorů ES6
  2. Hlubší potápění s generátory ES6
  3. Asynchronizace s generátory ES6
  4. Souběh s generátory ES6

Pokud jste četli a strávili část 1, část 2 a část 3 této série blogových příspěvků, pravděpodobně se v tuto chvíli cítíte docela sebejistě s generátory ES6. Doufejme, že vás inspiruje k tomu, abyste to skutečně posunuli a viděli, co s nimi můžete dělat.

Naším posledním tématem k prozkoumání je něco, co vám může trochu pokřivit mozek (stále točí můj, TBH). Udělejte si čas na propracování a přemýšlení o těchto konceptech a příkladech. Rozhodně si přečtěte další články na toto téma.

Investice, kterou zde vložíte, se vám z dlouhodobého hlediska opravdu vrátí. Jsem zcela přesvědčen, že budoucnost sofistikovaných asynchronních schopností v JS vzejde z těchto myšlenek.

Formální CSP (Communicating Sequential Processes)

Za prvé, jsem v tomto tématu zcela inspirován téměř výhradně díky fantastické práci Davida Nolena @swannodette. Vážně, přečtěte si, co k tématu napíše. Zde je několik odkazů, jak začít:

  • "Komunikace sekvenčních procesů"
  • "Generátory ES6 poskytují souběžnost stylu Go"
  • "Procesy extrahování"

Dobře, teď k mému průzkumu tématu. Nepřicházím do JS z formálního prostředí v Clojure, ani nemám žádné zkušenosti s Go nebo ClojureScriptem. Zjistil jsem, že se v těch čteních rychle ztrácím a musel jsem hodně experimentovat a poučovat se, abych z toho získal užitečné kousky.

V tomto procesu si myslím, že jsem dospěl k něčemu, co má stejného ducha a sleduje stejné cíle, ale vychází z mnohem méně formálního způsobu myšlení.

Co jsem se pokusil udělat, je vytvořit jednodušší pohled na rozhraní API CSP (a ClojureScript core.async) ve stylu Go, při zachování (doufám!) většiny základních schopností. Je docela možné, že ti, kdo jsou v tomto tématu chytřejší než já, rychle uvidí věci, které jsem při svých dosavadních průzkumech postrádal. Pokud ano, doufám, že se moje průzkumy budou vyvíjet a posouvat dál, a o taková odhalení se s vámi čtenáři budu i nadále dělit!

Rozbití teorie CSP (trochu)

O čem je CSP? Co to znamená říct „komunikovat“? "Sekvenční"? Co jsou tyto "procesy"?

V první řadě CSP pochází z knihy Tonyho Hoara "Communicating Sequential Processes" . Jsou to těžké věci z teorie CS, ale pokud vás zajímá akademická stránka věci, je to nejlepší místo, kde začít. V žádném případě nebudu toto téma řešit opojným, esoterickým, počítačovým způsobem. Půjdu k tomu docela neformálně.

Začněme tedy „sekvenčně“. Toto je část, kterou byste již měli znát. Je to další způsob, jak mluvit o jednovláknovém chování a synchronizovaném kódu, který získáváme z generátorů ES6.

Pamatujte si, že generátory mají syntaxi takto:

function *main() {
    var x = yield 1;
    var y = yield x;
    var z = yield (y * 2);
}

Každý z těchto příkazů se provádí postupně (v pořadí), jeden po druhém. 07 klíčové slovo anotuje body v kódu, kde může dojít k blokovací pauze (blokování pouze ve smyslu samotného kódu generátoru, nikoli okolního programu!), ale to nemění nic na zpracování kódu v 15 . Dost snadné, že?

Dále si povíme něco o „procesech“. O co jde?

Generátor se v podstatě chová jako virtuální „proces“. Je to samostatný kus našeho programu, který by mohl, pokud by JavaScript takové věci umožňoval, běžet zcela paralelně se zbytkem programu.

Vlastně by to trochu popletlo věci. Pokud generátor přistupuje ke sdílené paměti (tedy pokud kromě svých vlastních vnitřních lokálních proměnných přistupuje i k „volným proměnným“), není tak nezávislý. Ale předpokládejme prozatím, že máme funkci generátoru, která nemá přístup k vnějším proměnným (takže FP teorie by to nazvala "kombinátor"). Takže by to teoreticky mohlo spustit v/jako svůj vlastní proces.

Ale řekli jsme „procesy“ – množné číslo – protože zde je důležité mít dvě nebo více spuštěných naráz . Jinými slovy, dva nebo více generátorů, které jsou spárovány dohromady, aby spolupracovaly na dokončení nějakého většího úkolu.

Proč samostatné generátory místo jednoho? Nejdůležitější důvod:oddělení schopností/obav . Pokud se můžete podívat na úlohu XYZ a rozdělit ji na dílčí dílčí úlohy, jako jsou X, Y a Z, pak implementace každé do vlastního generátoru vede ke kódu, který lze snadněji zdůvodnit a udržovat.

Toto je stejný druh uvažování, který používáte, když vezmete funkci jako 22 a rozdělit jej na 37 , 42 a 54 funkcí, kde 67 volání 73 a 86 volá 94 , atd. Funkce rozdělujeme do samostatných funkcí, abychom získali lepší oddělení kódu, což usnadňuje údržbu kódu.

Totéž můžeme udělat s více generátory.

Konečně "komunikace". O co jde? Z výše uvedeného – spolupráce – vyplývá, že pokud budou generátory spolupracovat, potřebují komunikační kanál (nejen přístup ke sdílenému okolnímu lexikálnímu rozsahu, ale skutečný sdílený komunikační kanál, ke kterému mají všichni výhradní přístup) .

Co prochází tímto komunikačním kanálem? Cokoli potřebujete odeslat (čísla, řetězce atd.). Ve skutečnosti ani nemusíte skutečně posílat zprávu přes kanál, abyste mohli komunikovat přes kanál. „Komunikace“ může být stejně jednoduchá jako koordinace – jako přenos řízení z jednoho na druhého.

Proč převádět kontrolu? Především proto, že JS je jednovláknový a doslova jen jeden z nich může být v daný okamžik aktivně spuštěn. Ostatní jsou pak ve stavu pozastaveného běhu, což znamená, že jsou uprostřed svých úkolů, ale jsou pouze pozastaveni a čekají, až budou v případě potřeby obnoveny.

Nezdá se být reálné, že by libovolné nezávislé "procesy" mohly kouzelně spolupracovat a komunikovat. Cíl volné spojky je obdivuhodný, ale nepraktický.

Místo toho se zdá, že každá úspěšná implementace CSP je záměrná faktorizace existující, dobře známé sady logiky pro problémovou doménu, kde je každá část navržena speciálně tak, aby dobře fungovala s ostatními částmi.

Možná se v tom úplně mýlím, ale zatím nevidím žádný pragmatický způsob, jak by se daly nějaké dvě funkce náhodného generátoru nějak snadno spojit do páru CSP. Oba by museli být navrženi tak, aby s tím druhým spolupracovali, shodli se na komunikačním protokolu atd.

CSP v JS

Existuje několik zajímavých výzkumů v teorii CSP aplikovaných na JS.

Výše zmíněný David Nolen má několik zajímavých projektů, včetně Om, stejně jako core.async. Knihovna Koa (pro node.js) má velmi zajímavý záběr, především díky svému 105 metoda. Další knihovnou, která je docela věrná rozhraní core.async/Go CSP API, je js-csp.

Určitě byste se měli podívat na tyto skvělé projekty, abyste viděli různé přístupy a příklady toho, jak se CSP v JS zkoumá.

asynquence 110 :Návrh CSP

Protože jsem se intenzivně snažil prozkoumat aplikaci vzoru souběžnosti CSP na svůj vlastní kód JS, bylo pro mě přirozené rozšířit svou asynchronní asynchronní knihovnu řízení toku o možnosti CSP.

Už jsem měl 127 plugin, který se stará o asynchronní běh generátorů (viz "Část 3:Async. s generátory"), takže mě napadlo, že by mohl být poměrně snadno rozšířen tak, aby zpracovával více generátorů současně způsobem podobným CSP.

První návrhová otázka, kterou jsem řešil:jak víte, který generátor dostane kontrolu další ?

Zdálo se mi příliš těžkopádné/nemotorné mít každý z nich nějaké ID o kterých ostatní musí vědět, aby mohli adresovat své zprávy nebo je řídit a přenášet explicitně do jiného procesu. Po různých experimentech jsem se rozhodl pro jednoduchý kruhový plánovací přístup. Pokud tedy spárujete tři generátory A, B a C, nejprve získá řízení A, poté B převezme řízení, když A převezme řízení, poté C, když B převezme řízení, pak znovu A atd.

Jak bychom ale vlastně měli přenést kontrolu? Mělo by pro to existovat explicitní API? Po mnoha experimentech jsem se opět rozhodl pro implicitnější přístup, který se zdá být (zcela náhodně) podobný tomu, jak to dělá Koa:každý generátor dostane odkaz na sdílený "token" -- 137 bude signalizovat přenos řízení.

Dalším problémem je, jak by měl kanál zpráv vypadat jako. Na jednom konci spektra máte docela formalizované komunikační API, jako je to v core.async a js-csp (146 a 159 ). Po vlastních experimentech jsem se přiklonil na druhý konec spektra, kde byl mnohem méně formální přístup (ani API, jen sdílená datová struktura jako 169 ) se zdálo být vhodné a dostatečné.

Rozhodl jsem se mít pole (nazvané 173 ), že se můžete libovolně rozhodnout, jak chcete podle potřeby plnit/vypouštět. Můžete 187 zprávy do pole, 192 zprávy mimo pole, určovat podle konvence konkrétní sloty v poli pro různé zprávy, vkládat do těchto slotů složitější datové struktury atd.

Mám podezření, že některé úkoly budou vyžadovat opravdu jednoduché předávání zpráv a některé budou mnohem složitější, takže místo vnucování složitosti jednoduchým případům jsem se rozhodl neformalizovat kanál zpráv nad rámec 203 (a tedy žádné API kromě toho 212 sami). Je snadné přidat další formalismus k mechanismu předávání zpráv v případech, kdy to považujete za užitečné (viz stavový stroj příklad níže).

Nakonec jsem si všiml, že tyto „procesy“ generátorů stále těží z asynchronních schopností, které mohou používat samostatné generátory. Jinými slovy, pokud místo 224 po vyjmutí ovládacího tokenu získáte 235 splnění slibu (nebo asynquence sekvence), 248 mechanismus se skutečně pozastaví a čeká na budoucí hodnotu, ale nepřevede kontrolu -- místo toho vrátí výslednou hodnotu zpět aktuálnímu procesu (generátoru), takže si zachová kontrolu.

Tento poslední bod by mohl být (pokud si věci vykládám správně) nejkontroverznější nebo nepodobný ostatním knihovnám v tomto prostoru. Zdá se, že skutečný CSP tak trochu ohrnuje nos nad takovými přístupy. Zjistil jsem však, že mít tuto možnost k dispozici je velmi, velmi užitečné.

Příklad Silly FooBar

Dost teorie. Pojďme se ponořit do nějakého kódu:

// Note: omitting fictional `multBy20(..)` and
// `addTo2(..)` asynchronous-math functions, for brevity

function *foo(token) {
    // grab message off the top of the channel
    var value = token.messages.pop(); // 2

    // put another message onto the channel
    // `multBy20(..)` is a promise-generating function
    // that multiplies a value by `20` after some delay
    token.messages.push( yield multBy20( value ) );

    // transfer control
    yield token;

    // a final message from the CSP run
    yield "meaning of life: " + token.messages[0];
}

function *bar(token) {
    // grab message off the top of the channel
    var value = token.messages.pop(); // 40

    // put another message onto the channel
    // `addTo2(..)` is a promise-generating function
    // that adds value to `2` after some delay
    token.messages.push( yield addTo2( value ) );

    // transfer control
    yield token;
}

OK, takže jsou tu naše dva generátorové "procesy", 252 a 262 . Všimnete si, že oběma je předáno 274 objekt (můžete to nazvat jak chcete, samozřejmě). 286 vlastnost na 295 je náš sdílený kanál zpráv. Začíná se vyplněnými zprávami, které mu byly předány od inicializace našeho běhu CSP (viz níže).

301 explicitně přenese řízení na "další" generátor (pořadí round-robin). Nicméně 314 a 327 oba poskytují sliby (z těchto fiktivních zpožděných matematických funkcí), což znamená, že generátor je v tu chvíli pozastaven, dokud se slib nedokončí. Po vyřešení slibu se generátor, který je v současné době pod kontrolou, zvedne a pokračuje v chodu.

Bez ohledu na konečný 331 ed hodnota je v tomto případě 345 výraz, to je zpráva o dokončení našeho běhu CSP (viz níže).

Nyní, když máme naše dva generátory procesů CSP, jak je spustíme? Pomocí asynkvence :

// start out a sequence with the initial message value of `2`
ASQ( 2 )

// run the two CSP processes paired together
.runner(
    foo,
    bar
)

// whatever message we get out, pass it onto the next
// step in our sequence
.val( function(msg){
    console.log( msg ); // "meaning of life: 42"
} );

Je zřejmé, že jde o triviální příklad. Ale myslím, že to ilustruje koncepty docela dobře.

Nyní je možná vhodná chvíle to zkusit sami (zkuste změnit hodnoty kolem!), abyste se ujistili, že tyto koncepty dávají smysl a že si to můžete sami naprogramovat!

Další ukázkový příklad hračky

Podívejme se nyní na jeden z klasických příkladů CSP, ale pojďme na to z jednoduchých pozorování, které jsem dosud učinil, spíše než z akademicko-puristické perspektivy, z níž je obvykle odvozen.

Ping-pong . Jaká zábavná hra, co!? Je to můj oblíbený sport .

Představme si, že jste implementovali kód, který hraje ping-pong. Máte smyčku, která spouští hru, a máte dva kusy kódu (například větve v 354 nebo 362 prohlášení), že každý zastupuje příslušného hráče.

Váš kód funguje dobře a vaše hra běží jako mistrovský pingpong!

Ale co jsem si všiml výše o tom, proč je CSP užitečný? Oddělení zájmů/schopností. Jaké jsou naše samostatné schopnosti ve hře ping-pong? Dva hráči!

Takže bychom mohli na velmi vysoké úrovni modelovat naši hru se dvěma „procesy“ (generátory), jedním pro každého hráče . Když se dostaneme do podrobností, uvědomíme si, že „lepící kód“, který míchá kontrolu mezi dvěma hráči, je úkol sám o sobě, a toto kód by mohl být ve třetím generátoru, který bychom mohli modelovat jako rozhodčí hry .

Přeskočíme všechny druhy otázek specifických pro doménu, jako je bodování, herní mechanika, fyzika, herní strategie, AI, ovládání atd. Jediná část, na které nám záleží, je opravdu jen simulace pingu tam a zpět ( což je vlastně naše metafora pro CSP control-transfer).

Chcete vidět demo? Spusťte jej nyní (poznámka:použijte velmi nedávný večerníček FF nebo Chrome s podporou JavaScriptu ES6, abyste viděli, jak generátory fungují)

Nyní se podívejme na kód kousek po kousku.

Za prvé, co dělá asynquence sekvence vypadat?

ASQ(
    ["ping","pong"], // player names
    { hits: 0 } // the ball
)
.runner(
    referee,
    player,
    player
)
.val( function(msg){
    message( "referee", msg );
} );

Naši sekvenci jsme nastavili se dvěma úvodními zprávami:378 a 380 . K těm se za chvíli dostaneme.

Poté jsme nastavili CSP běh 3 procesů (korutin):390 a dvě 409 instance.

Závěrečná zpráva na konci hry je předána dalšímu kroku v naší sekvenci, kterou pak odešleme jako zprávu od rozhodčího .

Provedení rozhodčího:

function *referee(table){
    var alarm = false;

    // referee sets an alarm timer for the game on
    // his stopwatch (10 seconds)
    setTimeout( function(){ alarm = true; }, 10000 );

    // keep the game going until the stopwatch
    // alarm sounds
    while (!alarm) {
        // let the players keep playing
        yield table;
    }

    // signal to players that the game is over
    table.messages[2] = "CLOSED";

    // what does the referee say?
    yield "Time's up!";
}

Zavolal jsem ovládací token 410 aby odpovídala problémové doméně (ping-pongová hra). Je to pěkná sémantika, že hráč „podá stůl“ druhému, když odpálí míč zpět, ne?

427 smyčka v 436 jen stále poskytuje 447 zpět k hráčům, dokud se nespustil alarm na jeho stopkách. Když se tak stane, převezme řízení a prohlásí hru za ukončenou pomocí 453 .

Nyní se podívejme na 466 generátor (který používáme dvě instance):

function *player(table) {
    var name = table.messages[0].shift();
    var ball = table.messages[1];

    while (table.messages[2] !== "CLOSED") {
        // hit the ball
        ball.hits++;
        message( name, ball.hits );

        // artificial delay as ball goes back to other player
        yield ASQ.after( 500 );

        // game still going?
        if (table.messages[2] !== "CLOSED") {
            // ball's now back in other player's court
            yield table;
        }
    }

    message( name, "Game over!" );
}

První hráč odstraní své jméno z pole první zprávy (476 ), pak druhý hráč převezme jeho jméno (489 ), aby se oba mohli správně identifikovat. Oba přehrávače také uchovávají odkaz na sdílený 499 objekt (s jeho 500 čítač).

Zatímco hráči ještě neslyšeli závěrečnou zprávu od rozhodčího, "zasáhli" 518 zvýšením jeho 523 čítače (a vydají zprávu, která to oznámí), pak čekají na 536 ms (jen pro předstírání míče ne cestování rychlostí světla!).

Pokud hra stále pokračuje, pak „vydají stůl“ zpět druhému hráči.

To je ono!

Podívejte se na kód ukázky a získejte kompletní výpis kódu v kontextu, abyste viděli, jak všechny části spolupracují.

State Machine:Generator Coroutines

Jeden poslední příklad:definování stavového automatu jako sady generátorových korutin, které jsou řízeny jednoduchým pomocníkem.

Ukázka (poznámka:použijte nejnovější večer FF nebo Chrome s podporou ES6 JavaScript, abyste viděli, jak generátory fungují)

Nejprve si definujme pomocníka pro ovládání našich obslužných programů konečných stavů:

function state(val,handler) {
    // make a coroutine handler (wrapper) for this state
    return function*(token) {
        // state transition handler
        function transition(to) {
            token.messages[0] = to;
        }

        // default initial state (if none set yet)
        if (token.messages.length < 1) {
            token.messages[0] = val;
        }

        // keep going until final state (false) is reached
        while (token.messages[0] !== false) {
            // current state matches this handler?
            if (token.messages[0] === val) {
                // delegate to state handler
                yield *handler( transition );
            }

            // transfer control to another state handler?
            if (token.messages[0] !== false) {
                yield token;
            }
        }
    };
}

Tento 545 pomocná utilita vytvoří obálku generátoru delegování pro konkrétní hodnotu stavu, která automaticky spustí stavový stroj a přenese řízení při každém přechodu stavu.

Čistě konvencí jsem se rozhodl pro sdílený 552 slot bude obsahovat aktuální stav našeho státního automatu. To znamená, že počáteční stav můžete osít předáním zprávy z předchozího kroku sekvence. Pokud však žádná taková počáteční zpráva není předána, jednoduše se jako výchozí stav použije první definovaný stav. Podle konvence se také předpokládá, že konečný stav terminálu je 568 . To lze snadno změnit, jak uznáte za vhodné.

Stavové hodnoty mohou být libovolného typu:579 s, 583 s atd. Pokud lze hodnotu přísně testovat na rovnost s 598 , můžete jej použít pro své stavy.

V následujícím příkladu ukazuji stavový stroj, který přechází mezi čtyřmi 609 stavy hodnot v tomto konkrétním pořadí:612 . Pouze pro demo účely používá také čítač, takže může provést přechodovou smyčku více než jednou. Když náš generátor stavu konečně dosáhne stavu terminálu (624 ), asynkvence sekvence se přesune na další krok, přesně jak byste očekávali.

// counter (for demo purposes only)
var counter = 0;

ASQ( /* optional: initial state value */ )

// run our state machine, transitions: 1 -> 4 -> 3 -> 2
.runner(

    // state `1` handler
    state( 1, function*(transition){
        console.log( "in state 1" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 4 ); // goto state `4`
    } ),

    // state `2` handler
    state( 2, function*(transition){
        console.log( "in state 2" );
        yield ASQ.after( 1000 ); // pause state for 1s

        // for demo purposes only, keep going in a
        // state loop?
        if (++counter < 2) {
            yield transition( 1 ); // goto state `1`
        }
        // all done!
        else {
            yield "That's all folks!";
            yield transition( false ); // goto terminal state
        }
    } ),

    // state `3` handler
    state( 3, function*(transition){
        console.log( "in state 3" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 2 ); // goto state `2`
    } ),

    // state `4` handler
    state( 4, function*(transition){
        console.log( "in state 4" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 3 ); // goto state `3`
    } )

)

// state machine complete, so move on
.val(function(msg){
    console.log( msg );
});

Mělo by být docela snadné vysledovat, co se zde děje.

638 ukazuje, že tyto generátory mohou podle potřeby provádět jakoukoli asynchronní práci založenou na slibech/sekvencích, jak jsme viděli dříve. 640 je způsob, jakým přecházíme do nového stavu.

Naše 658 výše uvedený pomocník skutečně dělá těžkou práci zpracování 665 delegování a přechodové žonglování, takže naše státní manipulátory se vyjadřují velmi jednoduchým a přirozeným způsobem.

Přehled

Klíčem k CSP je spojení dvou nebo více generátorových „procesů“ dohromady, což jim dává sdílený komunikační kanál a způsob, jak mezi sebou přenášet kontrolu.

Existuje řada knihoven, které v JS víceméně zaujaly poměrně formální přístup, který odpovídá rozhraním Go a Clojure/ClojureScript API a/nebo sémantice. Všechny tyto knihovny mají za sebou opravdu chytré vývojáře a všechny představují skvělé zdroje pro další zkoumání/zkoumání.

asynquence se snaží zaujmout poněkud méně formální přístup a doufejme, že stále zachovává hlavní mechaniku. Když nic jiného, ​​asynquence 's 670 je docela snadné začít si hrát s generátory podobnými CSP, zatímco experimentujete a učíte se.

Nejlepší na tom je ale ta asynkvence CSP pracuje v souladu se zbytkem svých dalších asynchronních funkcí (přísliby, generátory, řízení toku atd.). Tímto způsobem získáte to nejlepší ze všech světů a můžete použít jakékoli nástroje vhodné pro daný úkol, to vše v jedné malé knihovně.

Nyní, když jsme v těchto posledních čtyřech příspěvcích prozkoumali generátory docela podrobně, doufám, že jste nadšeni a inspirováni k prozkoumání toho, jak můžete změnit svůj vlastní asynchronní kód JS! Co budete stavět pomocí generátorů?