標高+1m

Don't be rational.

sweet.jsでshift/reset: 限定継続を使ってコールバック地獄から抜け出す

f:id:ympbyc:20140111011737p:plain

Stop Writing JavaScript Compilers! Make Macros Insteadを読んで、sweet.jsを使ってみた。

解決したい事

JavaScriptでは非同期処理が多いので、継続渡しスタイルのコードばっかりになるのだけど、みんなこれをcallback hellとかpyramid of doomとか呼んで嫌っている。

そこでプロミスとかフューチャっていうのが使われるようになった。プロミスを使う利点は結果が出ていない状態をファーストクラスな値として使える事だと思う。ネストはなくなるけど、結局文法の支援がないからpromise.then(function () {...})みたいに、限定継続渡し的なことはしなくちゃいけない。

継続渡し形式が嫌ならcall/ccとかshift/resetを導入したらいいんじゃないかとはずっと思っていた。そこで今日は、sweet.jsでマクロを書いて、shift/reset的な事をやってみましたよという事を書く。

仕様

shiftは継続を取得する必要があるけれど、前置マクロだとreset(f(g(shift(k=>...))))f(g(<>))という継続はどうやっても取れないので、shiftは式ではなく文として実装することにする。そうすればその文の次の行からスコープの終わりまでが継続ということになるので、簡単に取得できる。

reset {
  ...
  shift_let <var> = <function>
  <continuation>...
}

こう書いたら<function>に限定継続が渡り、<function>の中で継続が呼び出されたらその引数が<var>に代入されるという仕様にした。

実装

Sweet.jsオンラインエディタ

macro reset {
  rule { {$exprs...} } => {
    (function () { $exprs... })()
  }
}

macro shift_let {
  rule { $var = $fn ; $rest...} => {
    $fn(function ($var) { $rest... })
  }
}

resetは関数スコープを作るだけで、shift_letは代入文をCPSに変換する。

使用例

reset {
  shift_let name = getName;
  shift_let data = (function (continuation) { 
      $.getJSON("http://.../users/" + name, continuation)
  });
  console.log(name);
  console.log(data);
}

//ヘルパ
function getName (continuation) {
  $("#some-input").on("change", function () {
    continuation($(this).val())
  })
}

resetの中で、非同期処理がフラットに書けている。

resetをうまく書けばもしかしたら(f(g(<>))みたいな継続も取れるかもしれないけれど、shift_letでも実用上十分だと思う。

まとめ

  • sweet.jsは素晴らしい
  • (限定)継続を取り出す構文があればコールバックを書かずに非同期処理が書ける
  • shift/resetっぽいことはマクロを使って実装できる

JavaScriptでマクロを使ってshift/resetっぽいことを実現してみました。 コールバック地獄を抜けるために限定継続を取り出す構文を使うこのアプローチは、素のjsから使えないという欠点を除けばプロミスよりも筋が良い気がします。GoのgoブロックとかClojure/ClojureScriptのcore.asyncなども同じドメインの問題を解決しようとして、マクロを使っています。(Goではマクロじゃなくてコンパイラが勝手に展開するんだと思うんですが同じ事です)

sweet.jsは最初にリンクした記事の通り、モジュラーにjsの文法を拡張していけるのでとても良いです。Schemeのパターンベースのマクロを知っている人もそうじゃない人も、例を見ればすぐにちょっとした面白いことができると思います。

僕はまだ30分くらい触っただけですが、sweet.jsには未来を感じました。みんなも使ってみるといいです。

sweet.js

追記: エゴサしてたらshift_letresetの直下にしか書けないって指摘を頂いていたのを見つけたので追記します。直下っていうと語弊があるかと思いますが、確かにresetの中にfunction書いてその中でshift_letしたらresetのとこまでの継続は取れなくてfunctionの終わりまでになっちゃいますね。というわけで文中の"shift/reset"を"shift/resetっぽいこと"に修正して回りました。