継続はコールバックの特別なケースだと思います。関数は、任意の数の関数を任意の回数コールバックできます。例えば:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
for (var i = 0; i < length; i++)
callback(array[i], array, i);
}
ただし、関数が最後に別の関数を呼び出す場合、2番目の関数は最初の関数の続きと呼ばれます。例えば:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
// This is the last thing forEach does
// cont is a continuation of forEach
cont(0);
function cont(index) {
if (index < length) {
callback(array[index], array, index);
// This is the last thing cont does
// cont is a continuation of itself
cont(++index);
}
}
}
関数が最後に別の関数を呼び出す場合、その関数は末尾呼び出しと呼ばれます。Schemeのような一部の言語は、末尾呼び出しの最適化を実行します。つまり、末尾呼び出しでは、関数呼び出しのオーバーヘッド全体が発生しません。代わりに、単純なgotoとして実装されています(呼び出し関数のスタックフレームが末尾呼び出しのスタックフレームに置き換えられています)。
おまけ:継続合格スタイルへ。次のプログラムを検討してください。
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
すべての演算(加算、乗算など)が関数の形式で記述されている場合、次のようになります。
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return add(square(x), square(y));
}
function square(x) {
return multiply(x, x);
}
function multiply(x, y) {
return x * y;
}
function add(x, y) {
return x + y;
}
さらに、値を返すことが許可されていない場合は、次のように継続を使用する必要があります。
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
値を返すことが許可されていない(したがって、継続を渡すことに頼らなければならない)このプログラミングのスタイルは、継続渡しスタイルと呼ばれます。
ただし、継続渡しスタイルには2つの問題があります。
- 継続を渡すと、呼び出しスタックのサイズが大きくなります。末尾呼び出しを排除するSchemeのような言語を使用しているのでなければ、スタックスペースが不足するリスクがあります。
- 入れ子関数を書くのは面倒です。
最初の問題は、継続を非同期に呼び出すことにより、JavaScriptで簡単に解決できます。継続を非同期で呼び出すことにより、関数は継続が呼び出される前に戻ります。したがって、呼び出しスタックのサイズは増加しません。
Function.prototype.async = async;
pythagoras.async(3, 4, console.log);
function pythagoras(x, y, cont) {
square.async(x, function (x_squared) {
square.async(y, function (y_squared) {
add.async(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply.async(x, x, cont);
}
function multiply(x, y, cont) {
cont.async(x * y);
}
function add(x, y, cont) {
cont.async(x + y);
}
function async() {
setTimeout.bind(null, this, 0).apply(null, arguments);
}
2番目の問題は、通常、とcall-with-current-continuation
略されると呼ばれる関数を使用して解決されcallcc
ます。残念ながらcallcc
、JavaScriptで完全に実装することはできませんが、そのほとんどのユースケースに代わる関数を作成できます。
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
function callcc(f) {
var cc = function (x) {
cc = x;
};
f(cc);
return cc;
}
callcc
関数は、関数を取るf
とそれを適用するcurrent-continuation
(と略しますcc
)。current-continuation
を呼び出した後、関数本体の残りの部分を包み込む継続関数ですcallcc
。
関数の本体を考えてみましょうpythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
current-continuation
第二のは、callcc
次のとおりです。
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
同様current-continuation
に、最初のcallcc
は次のとおりです。
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
以来current-continuation
最初のcallcc
別含まれcallcc
、それが継続渡しスタイルに変換する必要があります。
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
したがって、基本callcc
的には、論理的に関数本体全体を元の状態に変換します(そして、これらの無名関数に名前を付けますcc
)。このcallccの実装を使用するピタゴラス関数は、次のようになります。
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
繰り返しますがcallcc
、JavaScriptで実装することはできませんが、継続渡しスタイルをJavaScriptで次のように実装できます。
Function.prototype.async = async;
pythagoras.async(3, 4, console.log);
function pythagoras(x, y, cont) {
callcc.async(square.bind(null, x), function cc(x_squared) {
callcc.async(square.bind(null, y), function cc(y_squared) {
add.async(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply.async(x, x, cont);
}
function multiply(x, y, cont) {
cont.async(x * y);
}
function add(x, y, cont) {
cont.async(x + y);
}
function async() {
setTimeout.bind(null, this, 0).apply(null, arguments);
}
function callcc(f, cc) {
f.async(cc);
}
この関数callcc
は、try-catchブロック、コルーチン、ジェネレーター、ファイバーなどの複雑な制御フロー構造を実装するために使用できます。