最近、継続っぽいもの、継続の匂いがするものを取り上げてます。
- 「Erlangメッセージによる割り込み -- 継続への入り口として」 : ある関数の実行が中断して、しかも制御が戻らないにも関わらず、あたかも制御が戻るように見える現象を紹介しました。(続きを行う別関数に制御が渡ることにより実現されます。)
- 「継続を渡してチェーンすること」 : チェーンしかできない状況で、チェーン先にラベルを渡して呼び出しをシミュレートしてみました。
今日も、なーんか“継続みたいなもの”を話題にします。
非同期 Remote Procedure Call
まず次のような、HTMLに埋め込まれたJavaScriptプログラムを考えます。
<html>
<head>
<title>Difficult Calc</title>
<script>
function difficultCalc(x, y) {
var ans;
// とても難しい計算
return ans;
}function doDifficultCalc() {
var x = parseInt(document.getElementById("x").value, 10);
var y = parseInt(document.getElementById("y").value, 10);
if (isNaN(x) || isNaN(y)) {
alert("XとYは数値でなくてはなりません.");
throw "Error 'Not a Number'";
}
var resultPanel = document.getElementById("resultPanel");
resultPanel.innerHTML = "少々お待ちください";
var answer = difficultCalc(x, y); // 計算!
resultPanel.innerHTML = "答: " + answer;
}
</script>
</head>
<body>Xの値:<input id="x" type="text" value="0" ><br>
Yの値:<input id="y" type="text" value="0" ><br>
<button id="do" onclick="doDifficultCalc()">計算実行</button><br>
<span id="resultPanel">ここに答</span></body>
</html>
関数difficultCalcをハッキリとは書いてませんが、とても難しく複雑で、うんと時間がかかる計算だとします。そうすると、「計算実行」ボタンを押してから、ユーザーはしばらく待たされることになります。これはよろしくないですね。
そもそも、difficultCalcをJavaScriptでやるには無理があると判断して、XMLHttpRequestを使って計算サーバーにRPC(Remote Procedure Call)することにしましょう。そうしても、関数difficultCalcが計算サーバーからの応答をジッと待っていると、事情はあまり改善されません。XMLHttpRequestは、非同期HTTP要求をサポートする(つうか、ほとんどの場合は非同期要求を使う)ので、difficultCalcを次のように書き換えます。
function difficultCalc(x, y, callback) {
var url = "http://calc.example.jp/difficult"; // 計算サーバーURL
var xhr; // XMLHttpRequestオブジェクトが入るfunction runCallback() {
var ans;
if (xhr.status == 200 && xhr.readyState == 4) {
var text = xhr.responseText;
alert(text); // デバッグ・テスト用
ans = parseInt(text, 10);
callback(ans); // コールバックがここで呼ばれる
}
}
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} else {
throw "No XMLHttpRequest";
}
var params = "?x=" + x + "&y=" + y;
xhr.open('GET', url + params, true); // GETでいいとしましょ
xhr.onreadystatechange = runCallback;
xhr.send(null);
}
新しいdifficultCalcは3番目の引数を持ちますが、これは関数引数です。非同期呼び出しの結果が戻ってきたとき、第3引数に指定されたコールバック関数が呼び出されることになります。コールバック関数には計算結果が渡されるので、その結果を使って作業をするわけですね。
difficultCalcの呼び出しは、計算サーバーに要求を発行するとすぐに戻ります。インターネットの行き帰りや計算サーバー側処理に時間がかかっても、そのあいだJavaScriptはユーザーの相手をしたり別な仕事をすることができます。
続きの処理を引数に渡す
最初に出した同期版difficultCalcを使ってやっていたことを、非同期版difficultCalcを使って書き直しましょう。同期版の一部を抜粋すると:
var resultPanel = document.getElementById("resultPanel");
resultPanel.innerHTML = "少々お待ちください";
var answer = difficultCalc(x, y); // 計算!
resultPanel.innerHTML = "答: " + answer;
同期difficultCalcを呼び出した後の(あるいは「引き続く」)処理は、
です。この1行を関数にまとめます。
resultPanel.innerHTML = "答: " + answer;
function displayAnswer(answer) {
var resultPanel = document.getElementById("resultPanel");
resultPanel.innerHTML = "答: " + answer;
}
この関数は、やっていることはもとの1行とまったく同じですが、局所変数resultPanelを使い回すことはできないので(もう違うスコープですから)、初期化処理をちゃんとやっています。この関数displayAnswer(answer)がやることはまさに、difficultCalcの同期呼び出しに引き続く処理です。
非同期版difficultCalcを使う場合は、次のようにします。
var resultPanel = document.getElementById("resultPanel");
resultPanel.innerHTML = "少々お待ちください";
difficultCalc(x, y, displayAnswer); // 引き続く処理を第3引数に渡す
普通の同期逐次処理なら「計算結果を待ち、さらにその後にやるべき処理」を、関数にまとめて引数に渡してしまったので、非同期difficultCalcはサッサッと戻ることができるのです。未来の時間を関数に閉じ込め、自分はそのぶん一気にワープ(未来をたぐり寄せる)しちゃう感じです。
継続は時間軸を加工する
僕は継続をうまく使えないので、あまりエラソーなことは言えませんが、雰囲気的なことを言うならば; 本来は線形に流れていく時間を、継続を使うと切ったり並べ替えたり貼り合わせたりできる感じなんです。
僕らは日常的に、線形で一本道の時間の流れのなかで暮らしているので、時間の加工にはなかなかなじめません。でも、if文による分岐とwhile文による繰り返しも、枝分かれする時間や戻って輪になる時間を作っているので、時間軸を非線形に加工しているとも言えます。「制御」というのは、時間軸の加工のことなんでしょう、たぶん。