Scala-Fortsetzungen anhand aussagekräftiger Beispiele
Definieren wir, from0to10
dass 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 , from0to10
und 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 from0to10
diese 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 return
aus der Fortsetzung die Kontrolle an den aufgerufenen Code zurückgibt from0to10
. In Scala endet es dort, wo der reset
Block endet (*).
Nun sehen wir, dass die Fortsetzung als deklariert ist cont: Int => Unit
. Warum? Wir rufen from0to10
als auf val x = from0to10()
und Int
sind die Art von Wert, zu dem geht x
. Unit
bedeutet, dass der Block danach reset
keinen 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 \n
nach jeder Zeile aus?
Mit dieser Funktion back
kö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
}
back
ruft 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 from0to10
im Binärcode , enthält aber im Quellcode auch die Zuweisungsanweisung val i =
. Es endet dort, wo der reset
Block endet, aber das Ende des reset
Blocks gibt die Kontrolle nicht an das erste zurück from0to10
. Das Ende des reset
Blocks 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.!) from0to10
Beendet wird, wird der gesamte reset
Block 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 reset
und shift
sind Fehlbezeichnungen. Diese Namen sollten besser für die bitweisen Operationen belassen werden. reset
Definiert Fortsetzungsgrenzen und shift
nimmt eine Fortsetzung vom Aufrufstapel.
Anmerkungen)
(*) In Scala endet die Fortsetzung dort, wo der reset
Block 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 reset
Blocks 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 shift
es 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 x
ist, wird eine Ausnahme ausgelöst und die Fortsetzung wird nicht aufgerufen. Wenn dies x
ungerade ist, wird die Ausnahme nicht ausgelöst und die Fortsetzung wird aufgerufen. Der obige Code wird gedruckt:
1 3 5 7 9