sweet.jsでshift/reset: 限定継続を使ってコールバック地獄から抜け出す
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>に代入されるという仕様にした。
実装
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には未来を感じました。みんなも使ってみるといいです。
追記: エゴサしてたらshift_let
がreset
の直下にしか書けないって指摘を頂いていたのを見つけたので追記します。直下っていうと語弊があるかと思いますが、確かにreset
の中にfunction
書いてその中でshift_let
したらreset
のとこまでの継続は取れなくてfunction
の終わりまでになっちゃいますね。というわけで文中の"shift
/reset
"を"shift
/reset
っぽいこと"に修正して回りました。