一言で言えば継続
継続は強力な抽象化です。それを強調させてください。継続は非常に強力な抽象化です。なぜ継続はそれほど強力なのですか?継続は単なる関数[1]であり、「関数には呼び出し可能であるという危険な性質がある」ためです。それについては後で詳しく説明します。
では、継続が単なる関数である場合、なぜそれほど特別なのでしょうか?
はい、継続は単なる関数です。ただし、継続を特別なものにしているのは、それが表すものです。継続は「残りの計算」(別名計算コンテキスト) を表します。たとえば、次のスキーム式を考えてみましょう。
(add1 (* 3 x))
; |_____|
; |
; computation
(add1 [])
; |_______|
; |
; context
ここで、計算には が穴を表す(* 3 x)
コンテキスト(add1 [])
があります。[]
計算の結果、穴をふさぐことができます。これは(add1 [result])
いくつかのように書かれていresult
ます。継続は、コンテキストの単なる表現です。たとえば、関数(lambda (result) (add1 result))
はコンテキストを表します(add1 [])
。
一方、計算(* 3 x)
は関数として表すこともできます。これは、計算のコンテキストを表す継続(lambda (context) (context (* 3 x)))
である関数として表されます。Haskell の型は、そのコンテキストではなく、計算自体を表すcontext
ことに注意してください。Cont
わかりましたが、継続がそれほど強力な理由は何ですか?
前に述べたように、継続は単なる関数であり、「関数には呼び出し可能であるという危険な性質があります」。特に、関数は 1 回だけでなく、任意に何度も呼び出されるか、まったく呼び出されないことさえあります。これが継続を非常に強力にするものです。
たとえば(* 3 x)
、関数として表された前述の計算を考えてみましょう。
(lambda (context)
(context (* 3 x)))
context
複数回申し込んだ場合はどうなりますか?次のように、結果を 2 倍にするために使用できます。
(lambda (context)
(+
(context (* 3 x))
(context (* 3 x))))
が与えられた場合、これcontext
はadd1
答えを生成します(* 2 (add1 (* 3 x)))
。
一方、応募しなかった場合はどうなるcontext
でしょうか。評価を短縮できます。
(lambda (context)
(* 3 x))
これはまさに何をするかcall/cc
です。コンテキストを無視して単に答えを返すことで、評価を短絡させます。たとえば、次のことを考慮してください。
(call/cc (lambda (short-circuit)
(add1 (short-circuit (* 3 x)))))
ここで、計算(* 3 x)
には context がありますadd1
。call/cc
しかし、計算の結果に(つまり)のコンテキストを適用することで、計算を短絡させましたshort-circuit
。したがって、元のコンテキスト (つまりadd1
) を無視して、回答を返しました。
どのようにcall/cc
実装されていますか?
継続について理解したところでcallCC
、Haskellでの の定義を見てみましょう。
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
-- |___________________________|
-- |
-- f
callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
-- __|___ |______________________|
-- | | |
-- (a -> r) short-circuit
k
は現在の継続 (つまり、式全体の継続) であることに注意してください。関数f
は への唯一の入力callCC
です。Cont r a
実行する計算全体を表すa を返します。これを適用してk
計算結果を取得します。
ただし、計算内ではshort-circuit
、評価を短絡したいときはいつでも呼び出すことができます。この関数は結果を受け取り、そのコンテキストを無視して現在の継続k
を結果に適用する計算を返し、それによって評価を短絡します。
JavaScript ですべてをまとめます。
Scheme の継続とは何かを理解しました。callCC
Haskell でどのように動作するかを理解しました。新たに発見した知識を使用して、継続をcallCC
JavaScript で実装しましょう。
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = prefix => x => console.log(prefix, x);
var add1 = x => Cont.of(x + 1);
callCC(short_circuit => short_circuit(15).bind(add1)).runCont(log("short"));
callCC(short_circuit => Cont.of(15).bind(add1)).runCont(log("no short"));
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
callCC
を実装するために使用できることに注意してくださいgoto
。
の機能により、 、コルーチン、さらにはcallCC
次のような任意の制御フロー構造を作成できます。throw
goto
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = (x, ms) => new Cont(k => setTimeout(_ => k(console.log(x)), ms));
var omega = x => x(x); // This is a very dangerous function. Run `omega(omega)`.
callCC(omega).bind(cc => log("loop", 1000).bind(_ => cc(cc))).runCont(x => x);
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
このコードは次と同等です。
forever:
delay(1000);
print("loop");
goto forever;
したがって、継続を扱うときは注意が必要です。
[1]通常、継続は第一級関数を使用して実装されます。ただし、Scheme のようなファーストクラスの継続を伴う言語では、継続と関数が区別されます。それにもかかわらず、継続が関数でなくても、呼び出し可能であるという点では関数に似ています。