Scala-Fortsetzungen anhand aussagekräftiger Beispiele
Definieren wir, from0to10dass die Idee der Iteration von 0 bis 10 ausgedrückt wird:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Jetzt,
reset {
val x = from0to10()
print(s"$x ")
}
println()
Drucke:
0 1 2 3 4 5 6 7 8 9 10
In der Tat brauchen wir nicht x:
reset {
print(s"${from0to10()} ")
}
println()
druckt das gleiche Ergebnis.
Und
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
druckt alle Paare:
(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)
Wie funktioniert das?
Es ist der genannte Code , from0to10und die Telefonvorwahl . In diesem Fall folgt der folgende Block reset. Einer der an den aufgerufenen Code übergebenen Parameter ist eine Rücksprungadresse, die anzeigt, welcher Teil des aufrufenden Codes noch nicht ausgeführt wurde (**). Dieser Teil des aufrufenden Codes ist die Fortsetzung . Der aufgerufene Code kann mit diesem Parameter alles tun, wofür er sich entscheidet: Übergeben Sie die Kontrolle an ihn, ignorieren Sie ihn oder rufen Sie ihn mehrmals auf. Hier wird from0to10diese Fortsetzung für jede Ganzzahl im Bereich 0..10 aufgerufen.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i) // call the continuation
}
}
Aber wo endet die Fortsetzung? Dies ist wichtig, da der letzte returnaus der Fortsetzung die Kontrolle an den aufgerufenen Code zurückgibt from0to10. In Scala endet es dort, wo der resetBlock endet (*).
Nun sehen wir, dass die Fortsetzung als deklariert ist cont: Int => Unit. Warum? Wir rufen from0to10als auf val x = from0to10()und Intsind die Art von Wert, zu dem geht x. Unitbedeutet, dass der Block danach resetkeinen Wert zurückgeben darf (andernfalls tritt ein Typfehler auf). Im Allgemeinen gibt es 4 Typensignaturen: Funktionseingabe, Fortsetzungseingabe, Fortsetzungsergebnis, Funktionsergebnis. Alle vier müssen mit dem Aufrufkontext übereinstimmen.
Oben haben wir Wertepaare gedruckt. Drucken wir die Multiplikationstabelle. Aber wie geben wir \nnach jeder Zeile aus?
Mit dieser Funktion backkönnen wir festlegen, was zu tun ist, wenn die Steuerung zurückkehrt, von der Fortsetzung bis zum Code, der sie aufgerufen hat.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
backruft zuerst seine Fortsetzung auf und führt dann die Aktion aus .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ") // printf-like formatted i*j
}
Es druckt:
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
Nun, jetzt ist es Zeit für ein paar Brain Twister. Es gibt zwei Anrufungen von from0to10. Was ist die Fortsetzung für die erste from0to10? Es folgt dem Aufruf von from0to10im Binärcode , enthält aber im Quellcode auch die Zuweisungsanweisung val i =. Es endet dort, wo der resetBlock endet, aber das Ende des resetBlocks gibt die Kontrolle nicht an das erste zurück from0to10. Das Ende des resetBlocks gibt die Kontrolle an die 2. zurück from0to10, die wiederum schließlich die Kontrolle an zurückgibt back, und es ist das back, was die Kontrolle an den ersten Aufruf von zurückgibt from0to10. Wenn der erste (ja! 1.!) from0to10Beendet wird, wird der gesamte resetBlock verlassen.
Eine solche Methode zur Rückgabe der Kontrolle wird als Backtracking bezeichnet . Es handelt sich um eine sehr alte Technik, die zumindest aus der Zeit der Prolog- und AI-orientierten Lisp-Derivate bekannt ist.
Die Namen resetund shiftsind Fehlbezeichnungen. Diese Namen sollten besser für die bitweisen Operationen belassen werden. resetDefiniert Fortsetzungsgrenzen und shiftnimmt eine Fortsetzung vom Aufrufstapel.
Anmerkungen)
(*) In Scala endet die Fortsetzung dort, wo der resetBlock endet. Ein anderer möglicher Ansatz wäre, es dort enden zu lassen, wo die Funktion endet.
(**) Einer der Parameter des aufgerufenen Codes ist eine Rücksprungadresse, die anzeigt, welcher Teil des aufrufenden Codes noch nicht ausgeführt wurde. Nun, in Scala wird dafür eine Folge von Absenderadressen verwendet. Wie viele? Alle Rücksprungadressen, die seit dem Betreten des resetBlocks auf dem Aufrufstapel platziert wurden .
UPD Teil 2
Fortsetzen verwerfen: Filtern
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont() // call continuation only for even numbers
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Dies druckt:
0 2 4 6 8 10
Lassen Sie uns zwei wichtige Operationen herausrechnen: die Fortsetzung verwerfen ( fail()) und die Kontrolle an sie weitergeben ( succ()):
// fail: just discard the continuation, force control to return back
def fail() = shift { (cont: Unit => Unit) => }
// succ: does nothing (well, passes control to the continuation), but has a funny signature
def succ():Unit @cpsParam[Unit,Unit] = { }
// def succ() = shift { (cont: Unit => Unit) => cont() }
Beide Versionen von succ()(oben) funktionieren. Es stellt sich heraus, dass shiftes eine lustige Signatur hat, und obwohl succ()es nichts tut, muss es diese Signatur für die Typbalance haben.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
wie erwartet wird gedruckt
0 2 4 6 8 10
Innerhalb einer Funktion succ()ist nicht erforderlich:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
wieder druckt es
0 2 4 6 8 10
Definieren wir nun onOdd()über onEven():
// negation: the hard way
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException() // return is not allowed here
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
Wenn oben gerade xist, wird eine Ausnahme ausgelöst und die Fortsetzung wird nicht aufgerufen. Wenn dies xungerade ist, wird die Ausnahme nicht ausgelöst und die Fortsetzung wird aufgerufen. Der obige Code wird gedruckt:
1 3 5 7 9