JavaScript
CoffeeScript
Node.js
非同期処理
継続

【保存版】制御構造別非同期プログラミング完全制覇(サーバサイドJavascript・CoffeeScript)

More than 3 years have passed since last update.

補遺書いた

ライブラリ化して、フロントエンドにも対応した。GitHubリポジトリ。で、npmに上げた。そして、フロントエンド向けの紹介も書いた。ついでに、「asyncライブラリじゃだめなの?」という問いへの答えは「駄目」だ。asyncはsetImmediateとかnextTickとかで制御を実現しているので、タイミングの問題およびパフォーマンスのロスを引き起こす。

はじめに

対象読者はJavascript中級者を想定していますが、上級者の方もざっと読んでみて下さい。実は中級者であることが判明するかも知れません。

非同期関数を使って順次処理を行う場合、継続で繋いでいきます。しかし、この継続による表現は、同期処理に比べて直感的ではないため、難しい処理はしなくても済むようにプログラミングする羽目になっているケースが少なくないのでは、と私は憂慮しています。わかんなきゃコルーチン使えばいいや、という向きもありましょうが、本来の記述の仕方を理解しないでコルーチンに頼ると、落とし穴に嵌まるような気がしています。基本的な制御構造については、標準的な継続チェーンで記述できるようなスキルを身につけた方が身のためです。

WEBを見渡してみて、Node.jsにおける非同期コードデザイン(原文:Asynchronous Code Design with Node.js (The Shine blog))みたいな文書を読んでも、非同期逐次forの書き方は書いていません。とても恐ろしいことです。ちまたのJS-erはみなそんなに高階関数に慣れているのでしょうか。そんなはずは、ない、と思うのです。

と、なんかかしこまって書き始めたが、性に合わん。いつも通りいく。継続で書くとき、「よく考えないと大変なことになる」とはよく語られるけど、大丈夫かな? みんなよく考えているんかな? 「色々諦めているんで大丈夫」ってことじゃないよね? へいきだよね? と言うことを確認するのが、このエントリだ。「お前らの非同期プログラミングは間違っている」にしようかと思ったけど、それは思いとどまった。でも、本心はソレだ。

「コールバック地獄」と呼ぶ人もいるが、地獄と言うほどのもんじゃない。敢えて言うなら「コールバック パラレルワールド」くらいなもんだ。ちょっと勝手が違うけれど、一通りの制御構造が書けるように視点をずらせば、そこにはいつもと大して変わらぬ営みがある。なんて強気に書いてみたけど、弱気な面もある。やっぱし _for とか、汚いと思うよ、正直。うん。それは認めざるを得ない。助けてCoffeeScript!

ここで、「制御構造別」のお品書きを書いておこう。
* 逐次実行
* 関数
* 再帰関数
* if,switch
* 例外処理
* メンバ巡回ループ
* リトライループ
例えばさ、同期コードを書くとき、これらの1個でも「すらすら書けない」という人がいたら、その人を「同期プログラミングができる」と見なすかい? つまり、そーゆーことだ。「ふつーのプログラミング」をするなら、これで全部。でも、どれが欠けても、プログラミングは不自由になる。このエントリを最後まで読んでみて、もし「オレって不自由?」と思ったら、反省して精進してくれ。「何、今さらこんな当たり前のこと書いてんの?」と思ったら、はてブでののしってくれ。それはそれでこの業界ではご褒美だ。

それから、CoffeeScriptのコアな界隈に繋がりのある御仁、CoffeeScriptはCoffeeScriptで、こーゆー(特にループ関連の)イディオムの簡潔な書き方をサポートするように働きかけてくれ。僕はもうエンジニアじゃないし、老後のオープンソースに取り組むにはまだ早い。

本文書における仮想コード表記

基本的にCoffeeScriptの文法に準じるよ。「CoffeeScriptって『function』とか『;』とか『{}』とか省略するだけでしょ? きもーい」とお感じの女子諸君、おしりぺちんだよ。ここでの仮想コードをJavascriptで書いてみると、イディオムにそれだけのタイプ数を割く意味に疑問を感じ始めると思ってる。

CoffeeScriptに慣れていない向きもあろうかと思うので、ざっと大事なところだけ。

仮想コード
#//★関数呼び出し
#//--CoffeeScript---------------------
foo bar,baz
foo(bar,baz) #//カッコは解釈可能なら省略OK
#//==Javascript=======================
foo(bar, baz);

#//★無名関数(関数呼び出しの引数として)
#//--CoffeeScript---------------------
foo bar,(str) ->
  console.log str
#//==Javascript=======================
foo(bar, function(str) {
  console.log(str);
});

#//--CoffeeScript---------------------
#//引数がないときは()を省略OK
foo bar,->
  console.log 'hoge'
#//==Javascript=======================
foo(bar, function() {
  console.log('hoge');
});

#//★即時関数
#//--CoffeeScript---------------------
((arg) ->
  console.log arg
)('hoge')
#//==Javascript=======================
(function(arg) {
  console.log(arg);
})('hoge');

#//--CoffeeScript---------------------
((func) ->
  hoge fuga
  func()
)(->
  foo bar
)
#//==Javascript=======================
(function(func) {
  hoge(fuga);
  func();
})(function() {
  foo(bar);
});

ざっとこんな感じの対応関係になっている。継続にまつわるイディオムは、正直CoffeeScriptでも煩雑だなあ、と思われるものも幾つかあるくらいだから、正直生Javascriptでは苦痛だ。CoffeeScriptだからこそついていける。

あと、Yコンビネータは、特に後半でしょっちゅう出てくるので、何度も書かない。引数の数が自由な、下記のものを用いる。なにそれ???となったら、まず「こっち」を読んでくれ。無名関数で再帰をするためには=任意の場所で非同期処理を繰り返すには、このYコンビネータが必須だ。

y_combi = (func) ->
  return ((p) ->
    return ->
      return func(p(p)).apply(this,arguments)
  )((p) ->
    return ->
      return func(p(p)).apply(this,arguments)
  )

また、コード例に関しては、仮想コードのように書かれているが、下記のような関数の定義をしておけば、CoffeeScriptとしてなるべく実行可能なようにしてみた。(実行可能でない例は、具体的な処理まで書くと、却って流れが分かりにくくなる例であって、実用的な例に書き換えてみることを勧めたい。理屈が分かれば容易なはずだから。実行可能でない例にはコードの左上に「仮想コード」と附記した

ここで分かるように、 大文字1文字名の関数は非同期関数、小文字1文字名の関数は同期関数 という前提を置いているので、その点はご承知頂きたい。

A = (func) -> console.log 'A'; setTimeout func, 500
B = (func) -> console.log 'B'; setTimeout func, 500
C = (func) -> console.log 'C'; setTimeout func, 500
D = (func) -> console.log 'D'; setTimeout func, 500
E = (func) -> console.log 'E'; setTimeout func, 500
F = (func) -> console.log 'F'; setTimeout func, 500
G = (func) -> console.log 'G'; setTimeout func, 500
H = (func) -> console.log 'H'; setTimeout func, 500
I = (func) -> console.log 'I'; setTimeout func, 500
PRINT = (str,func) -> console.log 'PRINT: '+str; setTimeout func, 500

a = -> console.log 'a'
b = -> console.log 'b'
c = -> console.log 'c'
d = -> console.log 'd'
e = -> console.log 'e'
f = -> console.log 'f'
g = -> console.log 'g'
h = -> console.log 'h'
i = -> console.log 'i'
print = (str) -> console.log 'print: '+str

本編だ!

というわけで、本編だ。

逐次実行

なんか初心者向けな話から始めると、読むべき人が読まずに去ってしまうかも知れないので言っておくと、逐次実行が本当に分かっていれば、その後の話は単なるそこからの論理的帰結なのだ。でも、ループ=再帰だよ、なんていうことがあまり自明じゃないし、私のQiitaのアクセス数を見ても、「複数の無名関数を引数にした時のレイアウト」とか、「無名関数の再帰」とか、本当は日常的に必要なはずの知識に関するエントリーへのアクセスが、他の記事に比べて少なすぎるというのも傍証だし、この文書に相当するようなテキストが世の中にあまりない、というのも別の傍証だ。必要なはずなのに。

同期処理の後に、非同期処理が来るのは良い

これ全体をひとつの非同期処理と見なせる

a
b
C ->
  D -> return

非同期関数の後に、同期処理は置かない

これも後述のヤリ捨て並行処理に近い処理になるが、c,dはAの実行が完了する前に評価されるだけであり、直感的でない。関数の中に入っていたりすると、思わぬタイミングで評価されるため、禁則にした方が良いと思う。

A ->
  B -> return
c
d
#基本的にNGと考える

非同期関数の後は、継続の中に入れる

で、こんな感じのコードになっていく。
(これ全体をひとつの非同期処理と見なせる)

A ->
  b
  c
  D ->
    E -> return

ヤリ捨て並行処理

この場合、A->B->C, D, Eの3本の並行処理が走ることになるが、それぞれのパスはそれぞれ自己責任で後始末をして終了する処理になっている必要がある。結果を取り出すことはできない(が、それでいい場合もままある)

ヤリ捨て並行処理をしなくてはならない場面もある。それは、下のコードで言うと、Dが内部でEventEmitter経由でイベントを発生させるコードを含み、Eがイベントハンドラで、続きの処理をその内部で行う場合だ。実例は node-postgresの記事 で書いたので参考にして欲しい。

A ->
  B ->
    C -> return
D -> return
E -> return

まとまった複数のブロックを順次実行

まとまった複数のブロックを順次実行する場合にはこんな風に書く。

((block2) ->
  A ->
    B ->
      C ->
        block2()
)( ->
  D ->
    E ->
      F ->
        G ->
          H ->
            I ->
              return
)

これじゃまだ粒度がでかいので、3つに分けるならこうだ。

((block2,block3) ->
  A ->
    B ->
      C ->
        block2(block3)
)((next) ->
  D ->
    E ->
      F ->
        next()
,->
  G ->
    H ->
      I ->
        return
)

ABCDEFGHIの順で実行される。ただ、先々のブロックまで最初に定義して順次受け渡す必要があるので、あんまり長いチェーンを書くと何のことやら、ということになるので、これは「カンマ記法のようなもの」だと思って欲しい。あんまり長く続けるもんじゃない。とは言え、この逐次実行の書き方は、後で出てくる書法の多くに繋がる記法なので、慣れておくと良い。

コードが膨張しそうになったら、早々に、こんな風に書き直そう。

P = (next) ->
  A ->
    B ->
      C ->
        next()
Q = (next) ->
  D ->
    E ->
      F ->
        next()
R = (next) ->
  G ->
    H ->
      I ->
        next()

P ->
  Q ->
    R -> return

これは、同期プログラミングでも同じことだが、関数を巨大化させるのは死への道だ。機能単位を見極めて、良い感じに括って外出ししよう。そして、この後に述べるif,selectやforなど、みな同じことで、複雑なロジックをどんどん無名関数で建て増しし続けることはお勧めできない。

関数

すでに触れてしまったが、非同期処理をひとつでも含んだ関数は、非同期関数となるので、継続が指定できない形の関数に入れてしまうと、その非同期関数は、既に述べたヤリ捨てになってしまう

そして、非同期関数を含む全ての関数は、その関数の実行後の処理を期待するならば、常に継続を用いて行う必要がある。非同期関数を含んでいるのに継続を引数に取らない関数があったなら、それは、「処理の終端でしか使用できない関数」なのだ。

さらに言うなら、これは関数単位だけではない。継続を呼ばないパスがある時点で、そのパスは終端なのだ。

非同期処理を含む関数

末尾のreturnにはあまり意味はない。特に、再帰をさせたいときは、直観的でないので注意

myfunc = (arg1,next) ->
  PRINT arg1,->
    A next
  return

ここで、returnを通るときは、もう、一連の処理を終えるときだ。returnが評価されるとき、そのreturnで値を返したとしても、その値を利用するチャンスはない。関数から出て「次へ行く」のは、あくまで「next()」なのだ。

nextを呼ばないパスの例

非同期関数にとって「if err then return」の表現は、「このエラーは基本的に処理しない」という意味だ。エラーはエラーを受け取った関数が処理し、正常系に復帰できるなら行い、さもなくば例外を投げるつもりの分岐処理(後述)をするのが、エラーを無視しない場合の適切な書法である。エラーを処理するフローの書き方は、if,switchについての項、さらには例外処理の項、そしてリトライループの項を参照して欲しい(また、例外処理の前に、if,switchについて見た方が良いと思う)

((err,next) ->
  if err then return
  B ->
    C next
)(->
  D ->
    E -> return
)

再帰関数

そこ!再帰関数なんて作らないよ、なんて、アホなこと言わない。
 同期処理→非同期処理
という写像を考えたとき
 逐次実行→継続
 繰り返し処理→再帰処理
となるのだ。だから、再帰とは付き合わざるを得ない。ループの項の導入でもっと丁寧に説明するが、すっきりとした対比じゃないか。ちなみに同期での再帰処理が大変なことになるかというと、そんなことはない。同期関数での再帰処理は、全て、スタックなどのデータ構造の工夫があれば繰り返し処理に書き換えることができる。証明を読んではいないけど、証明もされてる。なので、
 再帰処理→恐ろしげな何か?
とはならない。
 再帰処理(=繰り返し処理)→再帰処理
なのだ。ちなみに、余談だが、
 例外処理→条件分岐
に過ぎない。これも後述するので大丈夫。

非同期処理を含む再帰関数でも、nextとして「その後の処理」を受け取らなければならない。nextがないと、再帰のループから出て行くことができない。再帰を続けるか、完全に終わるか。になってしまう。それでよい場面は稀だろう。nextは、再帰呼び出し後に何らかの処理を必要とする全ての再帰関数に必要なのだ。

また、非同期処理を含む再帰関数で、Quicksortや2分木探索のように2回再帰パスを通る場合、両方のパスを並行に実行しても構わないが、デバッグは困難になる。だから最初は順次実行するように書くのが定石だ。つーか、ほんとに大丈夫かな。この手の注意、あんまり見かけないんだけど。

分岐なし再帰の「再帰呼び出し部分」(同期)

仮想コード
if more
  func err,arg1,arg2

相当する非同期の書き方はこれだ。

仮想コード
if more
  func err,arg1,arg2,next
else
  next()

分岐あり再帰の「再帰呼び出し部分」(同期)

仮想コード
if left
  func err,arg1,arg2
if right
  func err,arg2,arg3

相当する非同期の書き方はこれだ。逐次実行の最後に挙げた例を思い出してもらえれば、すぐに合点がいくだろう。

仮想コード
((localnext) ->
  if left
    func err,arg1,arg2,localnext
  else
    localnext()
)(->
  if right
    func err,arg2,arg3,next
  else
    next()
)

そういう使い方をすることを念頭に再帰関数を書くと、こんな形になるはずだ。

arr = [2,3,5,7,11]
yourfunc = y_combi (func) ->
  return (err,arg1,arg2,next) ->
    A ->
      B ->
        if arr.pop()
          func err,argx,argy,next
        else
          next()

ちなみに、呼び出し方が分かりにくいが、別に特別なことはない。

A ->
  yourfunc err,'foo','bar',->
    B ->
      C ->
        return

こんな感じで普通に使える。

全く再帰っぽくない例だが、一応そのまま動かせるサンプルも載せておこう。

arr = ['h','sketch','one-touch']
yourfunc = y_combi (func) ->
  return (arr,next) ->
    PRINT arr.pop(),->
      A ->
        B ->
          if arr.length > 0
            func arr,next
          else
            next()

#test

C ->
  yourfunc arr,->
    D ->
      E ->
        F ->

分岐

単に分岐するだけなら、継続の中で関数を呼び分ければ良い。しかし、問題は分岐よりも合流で、これは高階関数が使えないと綺麗に書くことは難しい。結局、Node.jsの開発の際に候補となったのがHaskellやJavascriptなどの(「関数型」と括るのは嘘になるが)高階関数が使える言語であったのはそういうことだろう。Cも候補だったようだが、まあ、Cはバッドノウハウ全開なら、CPUで実行できるもんである限り、何でも書けるからね。まあ、高級アセンブラみたいなもんだし。

if,switchでの分岐後の合流(1)

if~elseにしても、switch~whenにしても、分岐のどれかひとつを実行する、という意味では同じことだ。だから、合流の仕方も、一緒くたに考えることができる。下記みたいな感じにね。

if節のみ特別処理、else節はスルーするとき

xxx = false

A ->
  B ->
    ((func) ->
      if xxx
        C func
      else
        func()
    )( ->
      D ->
        E ->
          return
    )

if節とelse節、どちらも別のことをしてからelse節

xxx = false

A ->
  B ->
    ((func) ->
      if xxx
        C func
      else
        D func
    )( ->
      E ->
        F ->
          return
    )

if,switchでの分岐後の合流(2)

合流先が複数になる場合の書法は、単に即時関数の引数が複数になるだけだ。即時関数の引数宣言で必要な分岐先を宣言し、下の呼び出し引数の中で中身を記述する。

この後、もし合流するならば、合流先も即時関数の引数にして、そこに飛べば良いけど、これ以上分岐が増えるようなら、意味のある単位で分割して、まとまった非同期関数を作った方が良い。

A ->
  B ->
    ((func,badfunc) ->
      xxx = false
      yyy = false
      if xxx
        C ->
          if yyy
            D ->
              E func
          else
            F badfunc
      else
        zzz = true
        G ->
          if zzz
            H func
          else
            I badfunc
    )( ->
      #func
      J ->
        K ->
          return
    , ->
      #badfunc
      L ->
        M ->
          return

例外処理

「非同期では例外処理が使えない」という話は、ある意味で本当だが、本当じゃない。たとえば、Yahoo!に恨みはないが、これなんかは、なにやら不思議な話の展開をしている。継続で書くときにtry~catch相当の制御構造を作りたいなら、単に、catchの役割をする関数がアクセス可能であれば良いだけだ。元々関数をまたいだフローを作るにはどうしたらいいか、と言うことを考えながらコーディングしなくちゃ駄目、と言うのがここまでの話だったはず。ってことは、例外処理はif,switchと何ら変わりがないということだ。

単純な例外処理相当のフロー

まずはシンプルに例外処理とif,switchが同等であることを示す

((mycatch,myfinally) ->
  exc = {mycatch: mycatch, myfinally: myfinally, reason: []}
  A ->
    B ->
      C ->
        xxx = true
        if xxx
          exc.reason.push 'C is xxx'
          exc.mycatch exc
        else
          D ->
            E ->
              yyy = false
              if yyy
                exc.reason.push 'E is yyy'
                exc.mycatch exc
              else
                F ->
                  exc myfinally
)((exc) ->
  #mycatch
  reason = exc.reason.pop()
  if reason?
    G ->
      exc.mycatch exc
  else
    exc.myfinally()
,->
  #myfinally
  H ->
    I ->
      return
)

深いところでの例外スロー

じゃあ、B->C->D を関数に切り出した時はどうしたら良いんだろうね? 「アクセス可能」にすれば良いよね!

somefunc = (exc, next) ->
  B ->
    C ->
      xxx = true
      if xxx
        exc.reason.push 'C is xxx'
        exc.mycatch exc
      else
        D next


((mycatch,myfinally) ->
  exc = {mycatch: mycatch, myfinally: myfinally, reason: []}
  A ->
    somefunc exc,->
      E ->
        yyy = true
        if yyy
          exc.reason.push 'E is yyy'
          exc.mycatch exc
        else
          F ->
            exc.myfinally()
)((exc) ->
  #mycatch
  reason = exc.reason.pop()
  if reason?
    G ->
      exc.mycatch exc
  else
    exc.myfinally()
,->
  #myfinally
  H ->
    I ->
      return
)

他にも方法は有るだろうけど、切り出した関数に飛び先一覧を引き渡しておくのが、一番シンプルな解法かな。

本格的な例外処理フロー

本格的な例外となったら、try~catchブロックが入れ子になっていたりして複数あるものということで良いかな? 入れ子構造というのはつまりデータ構造としてはスタックということだよね。_throwでスタックを巻き戻すような仕組みになってりゃいいわけだ。ただし、なんつーか、「ブロックの順次実行」のイディオムからも分かるとおり、べた書きするためには、「先に行き先とかが判定できなきゃ行けない」というのがあるので、_tryで対応するエラーの種類を定義しないと行けない。ここは確かに言語サポートがないと何ともならんな。コードの互換性を保つのに苦労はなさげだから、CoffeeScriptレベルで対応できそうだけどな。

繰り言はともかく、try~catchの入れ子にするためには、excオブジェクトの仕様をちょっと変えて、スタックにしないといけない。_throwの中で、受け取った error が見つかるまで、スタックを巻き戻して飛び先をコントロールすれば良いだけだ。例えばこんな感じ。_tryのかっこ悪さはいかんともしがたい。あと、その気になれば直接myfinallyを呼びうる状態にあるが、それはぜひ勘弁して欲しいので、そこはなんとかならんもんか。_tryの中でディープコピーして、元の myfinally = null みたいな強硬手段が必要かも。

class exc
  _stack = []
  constructor: ->

  _try: (e_array,_catch,_finally) ->
    _stack.push({e_array:e_array,_catch:_catch,_finally:_finally})

  _throw: (_e) ->
    while (catchset = _stack.pop())
      result = catchset.e_array.indexOf _e
      if result != -1
        _stack.push catchset
        catchset._catch @,_e,catchset._finally
        return
    console.log 'uncaught exception: '+_e.toString()
    process.exit 1

  _finally: ->
    _stack.pop()._finally @


xxx = false
yyy = false
zzz = true


myexc = new exc

somefunc = (myexc, next) ->
  B ->
    C ->
      if xxx
        myexc._throw 'Cerr'
      else if zzz
        myexc._throw 'Illegal Use Of Hands'
      else
        D next

((myexc,e_arr,mycatch,myfinally) ->
  myexc._try(e_arr,mycatch,myfinally)

  A ->
    somefunc myexc,->
      E ->
        if yyy
          myexc._throw 'Eerr'
        else
          F ->
            myexc._finally()

)(myexc
,['Cerr','Eerr']
,(myexc,_e,myfinally) ->
  # mycatch

  PRINT _e,->
    G ->
      myexc._finally()

,(myexc) ->
  # myfinally

  H ->
    I ->
      return

)

せっかくだからクラスにした。このままだと、イリーガルユースオブハンズという面倒見きれない例外が投げられる。xxxやyyyを色々に変えてもらえれば良い。

ループ

さあ、お待ちかねのループ。

繰り返し「継続する」ということは、再帰する、と言うことだが、このことを、多少分かりやすく説明してみよう。

for i=0; i<5; i++
  a

とはつまり、こういうことで、

a
a
a
a
a

それを非同期に直すとこうだ。

A ->
  A ->
    A ->
      A ->
        A ->
          next()

ね? 冒頭の、同期処理の逐次処理が、非同期処理の継続になる、という話と繋がったでしょ? つまりはそういうことだ。並行実行による配列巡回はソレとはちょっと別なので、その話をしてから、「走査」を行う順次実行、そして、「リトライループ」に相当する逐次ループの話をしよう。

並行実行する配列巡回

並行実行する場合の合流の仕方は、これは一般的なものだ。ヤリ捨て並行実行のサンプルの場合も、このようなやり方で結果を回収し、合流することができる。「継続しないパスは終端である」ということを思い出して欲しい。ここでの終端は、分岐して並行実行に入ってしまった非同期処理のうち何本かを終わらせて、1本だけ生かしておく、というやり方を取っているのだ。そして、並行実行のサンプルを最初に挙げたのは、並行実行させ、回収する、という手法が、限定的だからだ。ループの中で並行実行を行うのは、ここに挙げたような複数の対象に等しく同じ処理を適用するeach的な動作に限られるだろう。

arr = [2,3,5,7,11,13]
result = []
count = 0
for str,index in arr
  ((localstr,func) ->
    A ->
      B ->
        C ->
          console.log localstr.toString()
          result.push(localstr)
          func()
  )(str,->
    if(result.length == arr.length)
      #終了処理
        D ->
          E ->
            return
  )

というわけで、ついでに「_each」関数を書いてみた。

_each = (obj, applyfunc, endfunc) ->
  ((nextc) ->
    isarr = ('Array' == Object.prototype.toString.call(obj).slice(8, -1))
    num = 0
    next = nextc()
    if isarr
      for val,index in obj
        num++
        applyfunc val,index,->
          next num
    else
      for key,val of obj
        num++
        applyfunc key,val,->
          next num
  )(->
    count = 0
    return (num,next) ->
      count++
      if count == num
        endfunc()
  )

#test

_each [1,2,3,4,5,6],(val,index,next) ->
  A ->
    B ->
      C -> next()
,->
  D ->
    E ->
      F ->

というわけで、eachがあればこのパターンはいんじゃね?

逐次実行する配列巡回

さて、再帰に書き直すだけなので、Yコンビネータで再帰にしたコードに終了条件とかをはさみ混んでやればOK。やってることはほとんど所謂C方式の for と同じなんだけど、イテレーションの際、この _for 関数の中で代入をやっちゃってるので、そこが for と違う点。なので、このまま _for_in と _for_of に応用するのは厳しい気がするので、ソレは別に作った方が良いよ。下に、_for_in, _for_of を一緒くたに実装した関数を作ったので参考にして欲しい。

#これは非同期逐次のforの共通する動作をまとめた関数
_for = (index, f_judge, f_iter, loopfunc, endfunc) ->
  (y_combi (func) ->
    return ->
      if f_judge index
        loopfunc index,->
          #_break
          endfunc index
        ,->
          #_next
          index = f_iter index
          func()
      else
        endfunc index
  )()

#で、こういう風に使う
_for 0, ((n) -> n < arr.length), ((n) -> n + 1), (i,_break,_next) ->
  A ->
    B ->
      if false
        _break()
      else
        C ->
          _next()
,(i) ->
  #終了後の続き処理
  D ->
    E ->
      return

配列巡回、オブジェクト巡回(Javascriptではあんまりそう呼ばないが敢えて言えば連想配列巡回)用にしてみると、こんな風だ。サンプルそのものが順次実行のイディオムで書かれているが、もうこれを読みにくいとは思うまい。

_for_in = (obj, loopfunc, endfunc) ->
  index = 0
  isarr = ('Array' == Object.prototype.toString.call(obj).slice(8, -1))
  if isarr
    indexlimit = obj.length
  else
    indexlimit = Object.keys(obj).length
  (y_combi (func) ->
    return ->
      if index < indexlimit
        if isarr
          loopfunc obj[index],index, ->
            index += 1
            func()
        else
          key = Object.keys(obj)[index]
          value = obj[key]
          loopfunc key, value, ->
            index += 1
            func()
      else
        endfunc()
  )()

#で、こういう風に使う
((next) ->
  ##(例1) Arrayの巡回
  arr = [2,3,5,7,11,13]
  _for_in arr, (val,index,next) ->
    PRINT val,->
      A ->
        B ->
          C next
  , ->
    #終了後の続き処理
    D ->
      E ->
        next()
)(->
  ##(例2) Objectの巡回
  arr = {'ani': 'brother', 'imouto': 'sister', 'oya': 'parent'}
  _for_in arr, (key,val,next) ->
    PRINT key+':'+val,->
      A ->
        B ->
          C next
  , ->
    #終了後の続き処理
    D ->
      E ->
        return
)

Yコンビネータいじってもっとかっちょいい書き方できないのかな。誰か! あと、これはさすがに汚いので、も少しきれいに書けるようなsugarくれ。Coffeeの中の人!

逐次実行によるリトライループ

endfuncにフラグを渡しているケース。trueの時は、break;で抜けたようなもん。もちろん、冒頭の(endfunc)を(endfunc,breakfunc)にして、後で2つ無名関数を並べてもよい。

仮想コード
((endfunc) ->
  ycombi (func) ->
    return () ->
      A ->
        #これはbreakみたいなもん
        if critical_bad
          endfunc true
        else
          B ->
            C ->
              #これもbreakみたいなもん
              if critical_bad 
                endfunc true
              #これはcontinueもしくはwhile条件続行
              else if bad
                func()
              #while条件不成立=正常終了がコレ
              else
                endfunc false
)((isQuit) ->
  #終了後の続き処理
  D ->
    E ->
      return
)

終わりに

これで以上。思っていることのカタマリは、なんというか一口サイズなのだが、それなりに分かりうるように(分かりやすく、とまでは言えないが)書くだけでもずいぶん言葉数が必要なもんだね。いつものように、この「終わりに」はどんどん長くなる可能性がある。追記が好きなのだ。

ま、ともあれ、これで「ふつうのプログラム」を書くのに必要な制御構造は一通り語れたはずだ。というわけで、「非同期プログラムが書ける」ということは、少なくとも、これだけのことはすらすらできるはずなんだぜ? どーよ?

と、煽り気味に締めた後でDeferredについて。「コルーチンが美味しいかどうか考えた」でも書いたけど、あれは「コールバック地獄」から抜け出すための道具なんかじゃないよ。冒頭に書いたとおり、「コールバック地獄」なんてない。DeferredはDeferredで、処理の部分部分を部品として、そのつなぎ合わせ方に味を加える、という「コールバック世界」ならではの新パターンだ。誰かがきっとデザインパターン的な名前を付けているはずだ「Defferred Pattern」ってね。

本編で書いたような非同期処理の普通の書き方に習熟した人なら、Deferredが難しい、とか、キモい、とか言わないよね。むしろ、「制御構造の書き方となんか似てる!」と思うはず。面影あるよねえ。Deferredを考えた人はよく分かってる。そりゃ当然そうに決まってるんだけどさ。