ES6 Generator實現協同程序

至此本系列的四篇文章翻譯完結,查看完整系列請移步blogs javascript

因爲我的能力知識有限,翻譯過程當中不免有紕漏和錯誤,望不吝指正issuejava

ES6 Generators: 完整系列node

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

若是你已經閱讀並消化了本系列的前三篇文章:第一篇第二篇第三篇,那麼在此時你已經對如何使用ES6 generator函數成竹在胸,而且我也衷心但願你可以受到前三篇文章的鼓舞,實際去使用一下generator函數(挑戰極限),探究其究竟可以幫助咱們完成什麼樣的工做。git

咱們最後一個探討的主題可能和一些前沿知識有關,甚至須要動腦筋纔可以理解(誠實的說,一開始我也有些迷糊)。花一些時間來練習和思考這些概念和示例。而且去實實在在的閱讀一些別人寫的關於此主題的文章。es6

此刻你花時間(投資)來弄懂這些概念對你長遠來看是有益的。而且我徹底深信在未來JS處理複雜異步的操做能力將從這些觀點中應運而生。github

正式的CSP(Communicating Sequential Processes)

起初,關於該主題的熱情我徹底受啓發於 David Nolen @swannodette的傑出工做。嚴格說來,我閱讀了他寫的關於該主題的全部文章。下面這些連接能夠幫助你對CSP有個初步瞭解:編程

OK,就我在該主題上面的研究而言,在開始寫JS代碼以前我並無編寫Clojure語言的背景,也沒有使用Go和ClojureScript語言的經驗。在閱讀上面文章的過程當中,我很快就發現我有一點弄不明白了,而不得不去作一些實驗性學習或者學究性的去思考,並從中獲取一些有用的知識。數組

在這個過程當中,我感受我達到了和做者相同的思惟境界,而且追求相同的目標,可是卻採起了另外一種不那麼正規的思惟方式。promise

我所努力並嘗試去構建一個更加簡單的Go語言風格的CSP(或者ClojureScript語言中的core.async)APIs,而且(我但願)竟可能的保留那些潛在的能力。在閱讀我文章的那些聰明的讀者必定可以容易的發現我對該主題研究中的一些缺陷和不足,若是這樣的話,我但願個人研究可以演進並持續發展下去,我也會堅持和我廣大的讀者分享我在CSP上的更多啓示。數據結構

分解 CSP 理論(一點點)

CSP到底是什麼呢?在CSP概念下講述的「communicating」、「Sequential」又是什麼意思呢?「processes」有表明什麼?

首先,CSP的概念是從Tony Hoare的"Communicating Sequential Processes"中首次被說起。這本書主要是一些CS理論上的東西,可是若是你對一些學術上的東西很感興趣,相信這本書是一個很好的開端。在關於CSP這一主題上我毫不會從一些頭疼的、難懂的計算機科學知識開始,我決定從一些非正式入口開始關於CSP的討論。

所以,讓咱們先從「sequential」這一律念入手,關於這部分你可能已經至關熟悉,這也是咱們曾經討論過的單線程行爲的另外一種表述或者說咱們在同步形式的ES6 generator函數中也曾遇到過。

回憶以下的generator函數語法:

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

上面代碼片斷中的語句都按順序一條接一條執行執行,同一時間不可以執行多條語句。yield 關鍵字表示代碼在該處將會被阻塞式暫停(阻塞的僅僅是 generator 函數代碼自己,而不是整個程序),可是這並無引發 *main() 函數內部自頂向下代碼的絲毫改變。是否是很簡單,難道不是嗎?

接下來,讓咱們討論下「processes」。「processes」到底是什麼呢?

本質上說,一個 generator 函數的做用至關於虛擬的「進程」。它是一段高度自控的程序,若是 JavaScript 容許的話,它可以和程序中的其餘代碼並行運行。

說實話,上面有一點捏造事實了,若是 generator 函數可以獲取到共享內存中的值(也就是說,若是它可以獲取到一些除它自己內部的局部變量外的「自由變量」),那麼它也就不那麼獨立了。可是如今讓咱們先假設咱們擁有一個 generator 函數,它不會去獲取函數外部的變量(在函數式編程中一般稱之爲「組合子」)。所以理論上 generator 函數能夠在其本身的進程中獨立運行。

可是咱們這兒所討論的是「processes」--複數形式--,由於更重要的是咱們擁有兩個或者多個的進程。換句話說,兩個或者多個 generator 函數一般會同時出如今咱們的代碼中,而後協做完成一些更加複雜的任務。

爲何將 generator 函數拆分爲多個而不是一個呢?最重要的緣由:實現功能和關注點的解耦。若是你如今正在着手一項 XYZ 的任務,你將這個任務拆分紅了一些子任務,如 X, Y和 Z,而且每個任務都經過一個 generator 函數實現,如今這樣的拆分和解耦使得你的代碼更加易懂且可維護性更高。

這個你將一個function XYZ()分解爲三個函數X(),Y(),Z(),而後在X()函數中調用Y(),在Y()函數中調用Z()的動機是同樣的,咱們將一個函數分解成多個函數,分離的代碼更加容易推理,同時也是的代碼可維護性加強。

咱們能夠經過多個 generator 函數來完成相同的事情

最後,「communicating」。這有表達什麼意思呢?他是從上面--協程—的概念中演進而來,協程的意思也就是說多個 generator 函數可能會相互協做,他們須要一個交流溝通的渠道(不只僅是可以從靜態做用域中獲取到共享的變量,同時是一個真實可以分享溝通的渠道,全部的 generator 函數都可以經過獨有的途徑與之交流)。

這個通訊渠道有哪些做用呢?實際上不論你想發送什麼數據(數字 number,字符串 strings 等),你實際上不須要經過渠道來實際發送消息來和渠道進行通訊。「Communication」和協做同樣簡單,就和將控制權在不一樣 generator 函數之間傳遞同樣。

爲何須要傳遞控制權?最主要的緣由是 JS是單線程的,在同一時間只容許一個 generator 函數的執行。其餘 generator 函數處於運行期間的暫停狀態,也就是說這些暫停的 generator 函數都在其任務執行過程當中停了下來,僅僅是停了下來,等待着在必要的時候從新啓動運行。

這並非說咱們實現了(譯者注:做者的意思應該是在沒有其餘庫的幫助下)任意獨立的「進程」能夠魔法般的進行協做和通訊。

相反,顯而易見的是任意成功得 CSP 實現都是精心策劃的,將現有的問題領域進行邏輯上的分解,每一塊在設計上都與其餘塊協調工做。// TODO 這一段好難翻譯啊。

我關於 CSP 的理解也許徹底錯了,可是在實際過程當中我並無看到兩個任意的 generator 函數可以以某種方式膠合在一塊兒成爲一個 CSP 模式,這兩個 generator 函數必然須要某些特殊的設計纔可以相互的通訊,好比雙方都遵照相同的通訊協議等。

經過 JS 實現 CSP 模式

在經過 JS 實現 CSP 理論的過程當中已經有一些有趣的探索了。

上文咱們說起的 David Nolen 有一些有趣的項目,包括 Omcore.asyncKoa經過其use(..)方法對 CSP 也有些有趣的嘗試。另一個庫 js-csp徹底忠實於 core.async/Go CSP API。

你應該切實的去瀏覽下上述的幾個傑出的項目,去發現經過 JS實現 CSP 的的不一樣途徑和實例的探討。

asynquence 中的 runner(..) 方法:爲 CSP 而設計

因爲我強烈地想要在個人 JS 代碼中運用 CSP 模式,很天然地想到了擴展我現有的異步控制流的庫asynquence ,爲其添加 CSP 處理能力。

我已經有了 runner(..)插件工具可以幫助我異步運行 generator 函數(參見第三篇文章Going Async With Generators),所以對於我來講,經過擴展該方法使得其具備像CSP 形式同樣處理多個 generator函數的能力變得相對容易不少。

首選我須要解決的設計問題:我怎樣知道下一個處理哪一個 generator 函數呢?

若是咱們在每一個 generator 函數上面添加相似 ID同樣的標示,這樣別的 generator 函數就可以很容易分清楚彼此,而且可以準確的將消息或者控制權傳遞給其餘進程,可是這種方法顯得累贅且冗餘。通過衆多嘗試後,我找到了一種簡便的方法,稱之爲「循環調度法」。若是你要處理一組三個的 generator 函數 A, B, C,A 首先得到控制權,當 A 調用 yield 表達式將控制權移交給 B,再後來 B 經過 yield 表達式將控制權移交給 C,一個循環後,控制權又從新回到了 A generator 函數,如此往復。

可是咱們究竟如何轉移控制權呢?是否須要一個明確的 API 來處理它呢?再次,通過衆多嘗試後,我找到了一個更加明確的途徑,該方法和Koa 處理有些相似(徹底是巧合):每個 generator 對同一個共享的「token」具備引用,yield表達式的做用僅僅是轉移控制權。

另一個問題,消息渠道究竟應該採起什麼樣的形式呢。一端的頻譜就是你將看到和 core.async 和 js-csp(put(..take(..))類似的 API 設計。通過個人嘗試後,我傾向於頻譜的另外一端,你將看到一個不那麼正式的途徑(甚至不是一個 API,僅僅是共享一個像array同樣的數據結構),可是它又是那麼的合適且有效。

我決定使用一個數組(稱做messages)來做爲消息渠道,你能夠採起任意必要的數組方法來填充/消耗數組。你可使用push()方法來想數組中推入消息,你也可使用pop()方法來將消息從數組中推出,你也能夠按照一些約定慣例想數組中插入不一樣的消息,這些消息也許是更加複雜的數據接口,等等。

個人疑慮是一些任務須要至關簡單的消息來傳遞,而另一些任務(消息)卻更加複雜,所以我沒有在這簡單的例子上面花費過多的精力,而是選擇了不去對 message 渠道進行格式化,它就是簡簡單單的一個數組。(所以也就沒有爲array自己設計特殊的 API)。同時,在你以爲格式化消息渠道有用的時候,你也能夠很容易的爲該消息傳遞機制添加格外的格式化(參見下面的狀態機的事例)。

最後,我發現這些 generator 函數「進程」依然受益於單獨的 generator 函數的異步能力。換句話說,若是你經過 yield 表達式不是傳遞的一個「control-token」,你經過 yield 表達式傳遞的一個 Promise (或者異步序列),runner(..)的運行機制會暫停並等待返回值,而且不會轉移控制權。他會將該返回值傳遞會當前進程(generator 函數)並保持該控制權。

上面最後一點(若是我說明得正確的話)是和其餘庫最具爭議的地方,從其餘庫看來,真是的 CSP 模式在 yield 表達式執行後移交控制權,然而,我發如今個人庫中我這樣處理卻至關有用。(譯者注:做者就是這樣自信)

一個簡單的 FooBar 例子

咱們已經理論充足了,讓咱們看一些代碼:

// 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,上面出現了兩個 generator「進程」,*foo()*bar()。你會發現這兩個進程都將操做token對象(固然,你能夠以你喜歡的方式稱呼它)。token對象上的messages屬性值就是咱們的共享的消息渠道。咱們能夠在 CSP 初始化運行的時候給它添加一些初始值。

yield token明確的將控制權轉一個「下一個」generator 函數(循環調度法)。而後yield multBy20(value)yield addTo2(value)兩個表達式都是傳遞的 promises(從上面虛構的延遲數學計算方法),這也意味着,generator 函數將在該處暫停知道 promise 完成。當 promise 被解決後(fulfill 或者 reject),當前掌管控制權的 generator 函數從新啓動繼續執行。

不管最終的 yield的值是什麼,在咱們的例子中yield "meaning of..."表達式的值,將是咱們 CSP 執行的最終返回數據。

如今咱們兩個 CSP 模式的 generator 進程,咱們怎麼運行他們呢?固然是使用 asynquence:

// 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"
} );

很明顯,上面僅是一個可有可無的例子,可是其也能足以很好的表達 CSP 的概念了。

如今是時候去嘗試一下上面的例子(嘗試着修改下值)來搞明白這一律唸的含義,進而可以編寫本身的 CSP 模式代碼。

另一個「玩具」演示用例

若是那咱們來看看最爲經典的 CSP 例子,可是但願你們從文章上面的解釋及發現來入手,而不是像一般狀況同樣,從一些學術純化論者的觀點中導出。

Ping-pong。多麼好玩的遊戲,啊!它也是我最喜歡的體育運動了。

讓咱們想象一下,你已經徹底實現了打乒乓球遊戲的代碼,你經過一個循環來運行這個遊戲,你有兩個片斷的代碼(一般,經過if或者switch語句來進行分支)來分別表明兩個玩家。

你的代碼運行良好,而且你的遊戲就像真是玩耍乒乓球同樣!

可是還記得爲何我說 CSP 模式是如此有用呢?它完成了關注點和功能模塊的分離。在上面的乒乓球遊戲中咱們怎麼分離的功能點呢?就是這兩位玩家!

所以,咱們能夠在一個比較高的層次上,經過兩個「進程」(generator 函數)來對咱們的遊戲建模,每一個進程表明一位玩家,咱們還須要關注一些細節問題,咱們很快就感受到還須要一些「膠水代碼」來在兩位玩家之間進行控制權的分配(交換),這些代碼能夠做爲第三個 generator 函數進程,咱們能夠稱之爲裁判員。

咱們已經消除了全部可能會遇到的與專業領域相關的問題,好比得分,遊戲機制,物理學常識,遊戲策略,電腦玩家,控制等。在咱們的用例中咱們只關心模擬玩耍乒乓球的反覆往復的過程,(這一過程也正隱喻了 CSP 模式中的轉移控制權)。

想要親自嘗試下演示用例?那就運行把(注意:使用最新每夜版 FF 或者 Chrome,而且帶有支持 ES6,來看看 generators 如何工做)

如今,讓咱們來一段一段的閱讀代碼。

首先,asynquence 序列長什麼樣呢?

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

咱們給咱們的序列設置了兩個初始值["ping", "pong"]{hits: 0}。咱們將在後面討論它們。

接下來,咱們設置 CSP 運行 3 個進程(協做程序):*referee() 和 兩個*player()實例。

遊戲最後的消息傳遞給了咱們序列的第二步,咱們將在序列第二步中輸出裁判傳遞的消息。

裁判進程的代碼實現:

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!";
}

咱們稱「控制中token」爲table,這正好和(乒乓球遊戲)專業領域中的稱呼想一致,這是一個很好的語義化,一個遊戲玩家經過用拍子將球「yields 傳遞 table」給另一個玩家,難道不夠形象嗎?

while循環的做用就是在*referee()進程中,只要警報器沒有吹響,他將不斷地經過 yield 表達式將 table 傳遞給玩家。當警報器吹響,他掌管了控制權,宣佈遊戲結束「時間到了」。

如今,讓咱們來看看*player()generator 函數(在咱們的代碼中咱們兩次使用了該實例):

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!" );
}

第一位玩家從消息數組中取得他的名字「ping」,而後,第二位玩家取得他的名字「pong」,這樣他們能夠很好的分辨彼此的身份。兩位玩家同時共享ball這個對象的引用(經過他的hits計數)。

只要玩家沒有從裁判口中聽到結束的消息,他們就將經過將計數器加一來「hit」ball(而且會輸入一條計數器消息),而後,等待500ms(僅僅是模擬乒乓球的飛行耗時,不要還覺得乒乓球以光速飛行呢)。

若是遊戲依然進行,遊戲玩家「yield 傳遞 table」給另一位玩家。

就是這樣!

查看一下演示用例的代碼獲取一份完整用例的代碼,看看不一樣代碼片斷之間是如何協同工做的。

狀態機:Generator 協同程序

最後一個例子,經過一個 generator 函數集合組成的協同程序來定義一個狀態機,這一協同程序都是經過一個簡單的工具函數來運行的。

演示用例(注意:使用最新的每夜版 FF 或者 Chrome,而且支持 ES6的語法特性,看看 generator 函數如何工做)

首先讓咱們來定義一個工具函數,來幫助咱們控制咱們有限的狀態:

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;
            }
        }
    };
}

state(..) 工具函數爲一個特殊的狀態值建立了一個generator 代理的上層封裝,它將自動的運行狀態機,而且在不一樣的狀態轉換下轉移控制權。

按照慣例來講,我已經決定使用的token.messages[0]中的共享數據插槽來儲存狀態機的當前狀態值,這也意味着你能夠在序列的前一個步驟來對該狀態值進行初始化,可是,若是沒有傳遞該初始化狀態,咱們簡單的在定義第一個狀態是將該狀態設置爲初始狀態。同時,按照慣例,最後終止的狀態值設置爲false。正如你認爲合適,也很容易改變該狀態。

狀態值能夠是多種數據格式之一,數字,字符串等等,只要改數據能夠經過嚴格的===來檢測相等性,你就可使用它來做爲狀態值。

在接下來的例子中,我展現了一個擁有四個數組狀態的狀態機,而且其運行運行:1 -> 4 -> 3 -> 2。該順序僅僅爲了演示所需,咱們使用了一個計數器來幫助咱們在不一樣狀態間可以屢次傳遞,當咱們的 generator 狀態機最終遇到了終止狀態false時,異步序列運行至下一個步驟,正如你所期待那樣。

// 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 );
});

上面代碼的運行機制是否是很是簡單。

yield ASQ.after(1000)表示這些 generator 函數能夠進行 promise/sequence等異步工做,正如咱們先前縮減,yield transition(..)告訴咱們怎樣將控制權傳遞給下一個狀態。

咱們的state(..)工具函數真實的完成了yield *代理這一艱難的工做,像變戲法同樣,使得咱們可以以一種簡單天然的形式來對狀態進行操控。

總結

CSP 模式的關鍵點在於將兩個或者多個 generator「進程」組合在一塊兒,併爲他們提供一個共享的通訊渠道,和一個在其彼此之間傳遞控制權的方法。

市面上已經有不少庫多多少少實現了GO 和 Clojure/ClojureScript APIs 相同或者相同語義的 CSP 模式。在這些庫的背後是一些聰明而富有創造力的開發者門,這些庫的出現,也意味着須要更大的資源投入以及研究。

asynquence 嘗試着經過着經過不那麼正式的方法卻依然但願給你們呈現 CSP 的運行機制,只不過,asynquence 的runner(..)方法使得了咱們經過 generator 模擬 CSP 模式變得如此簡單,正如你在本篇文章所學的那樣。

asynquence CSP 模式中最爲出色的部分就是你將全部的異步處理手段(promise,generators,flow control 等)以及剩下的有機的組合在了一塊兒,你不一樣異步處理結合在一塊兒,所以你能夠任何合適的手段來處理你的任務,並且,都在同一個小小的庫中。

如今,在結束該系列最後一篇文章後,咱們已經完成了對 generator 函數詳盡的研究,我所但願的是你可以在閱讀這些文章後有所啓發,並對你現有的代碼進行一次完全革命!你將會用 generator 函數創造什麼奇蹟呢?

相關文章
相關標籤/搜索