意味のある例によるScalaの継続
from0to10
0から10までの反復のアイデアを表すを定義しましょう。
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
さて、
reset {
val x = from0to10()
print(s"$x ")
}
println()
プリント:
0 1 2 3 4 5 6 7 8 9 10
実際、私たちは必要ありませんx
:
reset {
print(s"${from0to10()} ")
}
println()
同じ結果を出力します。
そして
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
すべてのペアを出力します:
(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10)
さて、それはどのように機能しますか?
あると呼ばれるコードは、from0to10
、および呼び出しコード。この場合、次のブロックですreset
。呼び出されたコードに渡されるパラメーターの1つは、呼び出し元のコードのどの部分がまだ実行されていないかを示す戻りアドレスです(**)。呼び出しコードのその部分は継続です。呼び出されたコードは、そのパラメーターを使用して、制御を渡すか、無視するか、複数回呼び出すかを決定できます。ここでfrom0to10
は、0..10の範囲の各整数の継続を呼び出します。
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
しかし、継続はどこで終わりますか?return
継続からの最後のコードが呼び出されたコードに制御を返すため、これは重要from0to10
です。Scalaでは、reset
ブロックが終了する場所で終了します(*)。
これで、継続がとして宣言されていることがわかりcont: Int => Unit
ます。どうして?from0to10
として呼び出しval x = from0to10()
、Int
はに移動する値のタイプですx
。Unit
後のブロックreset
は値を返さないようにする必要があることを意味します(そうしないと、タイプエラーが発生します)。一般に、関数入力、継続入力、継続結果、関数結果の4つのタイプのシグニチャがあります。4つすべてが呼び出しコンテキストと一致する必要があります。
上記では、値のペアを出力しました。九九を印刷してみましょう。しかし\n
、各行の後にどのように出力するのでしょうか?
この関数をback
使用すると、継続からそれを呼び出したコードまで、制御が戻ったときに何を実行する必要があるかを指定できます。
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
最初にその継続を呼び出し、次にアクションを実行します。
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
印刷します:
0 0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 10
0 2 4 6 8 10 12 14 16 18 20
0 3 6 9 12 15 18 21 24 27 30
0 4 8 12 16 20 24 28 32 36 40
0 5 10 15 20 25 30 35 40 45 50
0 6 12 18 24 30 36 42 48 54 60
0 7 14 21 28 35 42 49 56 63 70
0 8 16 24 32 40 48 56 64 72 80
0 9 18 27 36 45 54 63 72 81 90
0 10 20 30 40 50 60 70 80 90 100
さて、今はいくつかの脳のねじれの時間です。の呼び出しは2つありますfrom0to10
。最初の続きは何from0to10
ですか?バイナリコードfrom0to10
での呼び出しに続きますが、ソースコードでは代入ステートメントも含まれています。ブロックが終了するところで終了しますが、ブロックの終了は制御を最初のに戻しません。ブロックの終わりは制御を2番目に戻し、2番目は最終的に制御をに戻し、それは制御をの最初の呼び出しに戻します。最初の(yes!1st!)が終了すると、ブロック全体が終了します。val i =
reset
reset
from0to10
reset
from0to10
back
back
from0to10
from0to10
reset
制御を戻すこのような方法はバックトラッキングと呼ばれ、少なくともPrologおよびAI指向のLisp派生物の時代から知られている非常に古い手法です。
名前reset
とshift
は誤称です。これらの名前は、ビット演算用に残しておく必要があります。reset
継続境界を定義shift
し、呼び出しスタックから継続を取得します。
ノート)
(*)Scalaでは、継続はreset
ブロックが終了するところで終了します。別の可能なアプローチは、関数が終了する場所で終了させることです。
(**)呼び出されたコードのパラメーターの1つは、呼び出し元のコードのどの部分がまだ実行されていないかを示す戻りアドレスです。そうですね、Scalaでは、一連のリターンアドレスがそのために使用されます。幾つ?reset
ブロックに入ってからコールスタックに配置されたすべてのリターンアドレス。
UPDパート2
継続の破棄:フィルタリング
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
これは印刷します:
0 2 4 6 8 10
2つの重要な操作を除外しましょう:継続を破棄する(fail()
)とそれに制御を渡す(succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
succ()
(上記の)両方のバージョンが機能します。これは、ことが判明shift
面白い署名を持ち、かつもののsucc()
何もしません、それはタイプのバランスのためにその署名を持っている必要があります。
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
予想通り、印刷します
0 2 4 6 8 10
関数内でsucc()
は、必要ありません:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
再び、それは印刷します
0 2 4 6 8 10
さて、次のように定義onOdd()
しましょうonEven()
:
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException()
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
上記でx
は、偶数の場合、例外がスローされ、継続は呼び出されません。場合はx
奇数で、例外がスローされていないと継続が呼び出されます。上記のコードは次のように出力します。
1 3 5 7 9