解決策
node.jsはすべてを非ブロッキングI/Oで行うことで高度に並行なサーバーを容易にしたが、その代償は構造に現れた。データベース検索やAPI呼び出しのたびにコールバックの階層が増え、数万の長時間接続を保持するよう作られたkoush自身のプロジェクトでは、日常的に7、8段のインデントに達していた。単純なリクエストとクエリだけでも2つのネストしたコールバックが必要で、エラー処理は各階層をそれぞれのtry/catchで包み、ページ分割されたAPI呼び出しのような逐次処理は再帰的な継続関数になっていた。
2011年に利用可能だった修正策は問題を除去するのではなく移動させるものだった。yield演算子はイテレータを中断・再開できたが扱いにくいままで、TameJSのようなソース間トランスレータは一見きれいなコードをどこでも実行できる通常のJavaScriptにコンパイルしたが、その代償は開発者が書いたコードではなく生成されたコードをデバッグすることだった。koushの参照点はC# 5のasync/awaitであり、非同期コードを同期的に読めるようにする明示的な言語機能だった。
彼の取った道はその機能をランタイムに組み込むことだった。V8で実装されたコルーチン、async関数マーカー、そしてコールバック完了時に関数を終了して再開するawaitを備えたnode.jsのフォークである。パーサーは依然としてコールバック版とほぼ同じASTを生成したため、1つのtry/catchがすべてのawaitにまたがり、whileループがページ分割されたAPIリクエストを逐次化でき、ndbとnode-inspectorは元のコードを引き続きデバッグできた。代償はカスタムnodeビルドであり、自前のサーバーを管理するチームなら現実的に払えるものだった。
達成したこと
V8コルーチンによりawaitで関数を中断し、非同期コードが同期コードのように読めるようにした
うまくいった理由
ネストしたコールバックはI/Oの各ステップごとに増殖し、エラー処理を別々のスコープに押しやる。awaitはフロー全体の周りに1つのtry/catchを保つため、例外が同期コードのようにコールバック境界を越える。
ソース間トランスレーションではなくランタイム内の本物のコルーチンにより、開発者が書いたコードが実行されるコードのままとなり、ndbとnode-inspectorは生成された出力ではなく元のコードをデバッグする。
逐次に見える制御フローにより、通常のループが逐次化された非同期処理を表現できる。ページ分割されたGDATAの例では再帰的なチャンク関数をwhileループに置き換えている。
サーバーチームは自らのデプロイを管理するため、node.jsとV8をフォークすることは現実的な代償であり、この設計は提案ではなくテスト付きの動作するブランチとして出荷された。
応用できること
回避策が複雑さを生成コードやより深いネストに押しやる場合、言語レベルで修正せよ。I/Oで関数全体を中断し、読めるコードが実行されデバッグされるコードとなるようにする
その後
プロジェクトはマージされた機能ではなくフォークのままだった。25回の改訂を経て2013年4月に最後に編集されたwikiは、カスタムnodeビルドをデプロイの選択肢として位置づけ、そのTODOにはawaitを含むブランチの最適化が挙げられていた。2011年9月29日にHacker Newsに投稿されると222ポイントと101コメントを集め、読者はこのアプローチを非常にエレガントだと評し、適切な修正はコルーチンを導入することだと同意した。記録上、このパッチがnode.jsに入ったことを示すものはない。その目に見える効果は、動作するコードとともに、JavaScriptが言語レベルでasync/awaitをいかに獲得できるかを実証したことだった。
出典をたどる
出典
- "async" support in node.js (koush/node wiki) GitHub — koush/node wiki · github.com
- Fixing the callback spaghetti in node.js | Hacker News Hacker News · news.ycombinator.com ·