Webフロントエンドと継続

継続とは、特定の評価戦略の元で、計算の残りの部分を関数として取り出すもの。
alert("Hello, " + "world!");
ここで仮にJavaScriptにcall_ccというオペレータがあったとして、
alert(call_cc((k)=>k("Hello, ")) + "world!");
このとき、kには、call_ccが評価された時点での継続が束縛される、この時の継続は要するにこういう関数と考えて良い。
k = (val)=>alert(val+"world!");

さて、この継続kは何度でも再利用できる。

//リスティング1.1
alert(call_cc((k)=>{
  setTimeout(()=>k("Good bye, "), 2000);
  setTimeout(()=>k("Hello, "), 1000);
  k("Good morning, ");
}) + "world!");

こうすると、評価時にGood morning, world!、1秒後にHello, world!、2秒後にGoodbye, world!と表示が出る。

これのなにが嬉しいのか?

JavaScriptでは、実時間のかかる処理を非同期に行うために、コールバック関数が使われる。ある処理が完了した結果を受けて、残りの処理を実行するために、残りの処理を関数として記述する。これは専門用語で継続渡し形式と呼ばれる。

//リスティング2.1
document.querySelector("textarea").addEventListener("change", (e)=>{
  fetch(e.target.value).then((resp)=>resp.text()).then((text)=>{
    console.log(text);
  });
});

textareaに入力されたurlをGETして、内容の文字列をコンソールに出力するというコード。これだけの処理に3つコールバック関数が記述されている。
(e)=>{ fetch(...)... }
(resp)=>resp.text()
(text)=>{console.log(text)}
Promise以前のJavaScriptでは、非同期処理がネストするにつれて、俗にコールバック地獄と呼ばれた蟻地獄状のネストが乱発した。Promiseによって、ネストは隠蔽できるようになったが、Promiseオブジェクトの作成など、煩雑な点が多く、しかもコールバック関数の数自体は減らない。

上記のコードでcall_ccが使えたとしよう。継続渡し形式とcall_cc形式の適合のための下準備が必要になる。

//リスティング2.2 下準備
var idle;
call_cc((k)=>idle=k);
function listen (el, ev) {
  call_cc((k)=>{
    el.addEventListener(ev, k);
    idle();
  });
}
function fetch_text (url) {
  call_cc((k)=>{
    fetch(url).then((resp)=>resp.text()).then(k);
    idle();
  });
}

少々煩雑に思えるかもしれないが、適切なレベルで抽象化すれば、下準備を何度もする必要はない。
idleは、なにもしない継続で、これを使って、評価時に計算が進むのを止めて、非同期処理が帰ってきたときにだけ、処理が流れるようにするためのもの。

上記の下準備を終えると、リスティング2.1の処理は、以下のように書ける。

//リスティング2.3 メインロジック
console.log(fetch_text(listen("textarea", "change").target.value));

リスティング2.1と比較すると、圧縮の効果は歴然だ。処理系の都合に合わせるための形式が全く排除されて、必要なことだけが記述できる。
あたかもブロッキング処理のように見えるが、ちゃんと非同期に処理される。

非同期処理の箇所を明示したければ、リスティング2.2を適切に変更することで、以下のように冗長に書くこともできる。

console.log(call_cc(fetch_text(call_cc(listen("textarea", "change")).target.value)));

残念ながらJavaScriptにcall_ccはない。

今すぐにcall_ccを使いたければ、JavaScript上のSchemeインタープリタを使うという手がある。

BiwaScheme

BiwaSchemeの継続を使って、更に複雑なフローコントロールをする実例:
call/ccの合成の解 — 標高+1m

LISPing at the end of time.