Continuazioni di Scala tramite esempi significativi
Definiamo from0to10che esprime l'idea di iterazione da 0 a 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Adesso,
reset {
val x = from0to10()
print(s"$x ")
}
println()
stampe:
0 1 2 3 4 5 6 7 8 9 10
Non abbiamo infatti bisogno di x:
reset {
print(s"${from0to10()} ")
}
println()
stampa lo stesso risultato.
E
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
stampa tutte le coppie:
(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)
Ora, come funziona?
C'è il codice chiamato , from0to10e il codice di chiamata . In questo caso, è il blocco che segue reset. Uno dei parametri passati al codice chiamato è un indirizzo di ritorno che mostra quale parte del codice chiamante non è stata ancora eseguita (**). Quella parte del codice chiamante è la continuazione . Il codice chiamato può fare con quel parametro qualunque cosa decida di: passargli il controllo, o ignorarlo, o chiamarlo più volte. Qui from0to10chiama quella continuazione per ogni numero intero compreso tra 0 e 10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Ma dove finisce il seguito? Questo è importante perché l'ultima returndai rendimenti di continuazione il controllo al codice chiamato, from0to10. In Scala, finisce dove finisce il resetblocco (*).
Ora, vediamo che la continuazione è dichiarata come cont: Int => Unit. Perché? Invociamo from0to10as val x = from0to10(), ed Intè il tipo di valore a cui va x. Unitsignifica che il blocco dopo non resetdeve restituire alcun valore (altrimenti ci sarà un errore di tipo). In generale, ci sono 4 tipi di firme: input di funzione, input di continuazione, risultato di continuazione, risultato di funzione. Tutti e quattro devono corrispondere al contesto di chiamata.
Sopra, abbiamo stampato coppie di valori. Stampiamo la tavola pitagorica. Ma come produciamo l'output \ndopo ogni riga?
La funzione backci consente di specificare cosa deve essere fatto quando il controllo ritorna, dalla continuazione al codice che lo ha chiamato.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
backprima chiama la sua continuazione, quindi esegue l' azione .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
Stampa:
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
Bene, ora è il momento di scioccare il cervello. Ci sono due invocazioni di from0to10. Qual è la continuazione del primo from0to10? Segue l'invocazione di from0to10nel codice binario , ma nel codice sorgente include anche l'istruzione di assegnazione val i =. Termina dove finisce il resetblocco, ma la fine del resetblocco non restituisce il controllo al primo from0to10. La fine del resetblocco restituisce il controllo al 2 ° from0to10, che a sua volta alla fine restituisce il controllo a back, ed è backche restituisce il controllo alla prima invocazione di from0to10. Quando il primo (sì! 1 °!) from0to10Esce, si esce dall'intero resetblocco.
Tale metodo di restituzione del controllo è chiamato backtracking , è una tecnica molto antica, conosciuta almeno dai tempi dei derivati del Lisp orientati al Prolog e AI.
I nomi resete shiftsono nomi impropri. Questi nomi avrebbero dovuto essere lasciati meglio per le operazioni bit per bit. resetdefinisce i limiti di continuazione e shiftprende una continuazione dallo stack di chiamate.
Appunti)
(*) In Scala il seguito finisce dove finisce il resetblocco. Un altro approccio possibile sarebbe lasciarlo finire dove finisce la funzione.
(**) Uno dei parametri del codice chiamato è un indirizzo di ritorno che mostra quale parte del codice chiamante non è stata ancora eseguita. Bene, in Scala, per questo viene utilizzata una sequenza di indirizzi di ritorno. Quanti? Tutti gli indirizzi di ritorno inseriti nello stack di chiamate dall'ingresso nel resetblocco.
UPD Parte 2
Discarding Continuations: Filtering
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Questo stampa:
0 2 4 6 8 10
Consideriamo due importanti operazioni: scartare la continuation ( fail()) e passare il controllo ad essa ( succ()):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Entrambe le versioni di succ()(sopra) funzionano. Si scopre che shiftha una firma divertente e, sebbene succ()non faccia nulla, deve avere quella firma per l'equilibrio del tipo.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
come previsto, viene stampato
0 2 4 6 8 10
All'interno di una funzione succ()non è necessario:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
ancora una volta, stampa
0 2 4 6 8 10
Ora, definiamo onOdd()tramite 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 ")
}
Sopra, se xè pari, viene generata un'eccezione e la continuazione non viene chiamata; se xè dispari, l'eccezione non viene generata e viene chiamata la continuazione. Il codice sopra viene stampato:
1 3 5 7 9