Continuazioni di Scala tramite esempi significativi
Definiamo from0to10
che 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 , from0to10
e 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 from0to10
chiama 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 return
dai rendimenti di continuazione il controllo al codice chiamato, from0to10
. In Scala, finisce dove finisce il reset
blocco (*).
Ora, vediamo che la continuazione è dichiarata come cont: Int => Unit
. Perché? Invociamo from0to10
as val x = from0to10()
, ed Int
è il tipo di valore a cui va x
. Unit
significa che il blocco dopo non reset
deve 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 \n
dopo ogni riga?
La funzione back
ci 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
}
back
prima 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 from0to10
nel codice binario , ma nel codice sorgente include anche l'istruzione di assegnazione val i =
. Termina dove finisce il reset
blocco, ma la fine del reset
blocco non restituisce il controllo al primo from0to10
. La fine del reset
blocco restituisce il controllo al 2 ° from0to10
, che a sua volta alla fine restituisce il controllo a back
, ed è back
che restituisce il controllo alla prima invocazione di from0to10
. Quando il primo (sì! 1 °!) from0to10
Esce, si esce dall'intero reset
blocco.
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 reset
e shift
sono nomi impropri. Questi nomi avrebbero dovuto essere lasciati meglio per le operazioni bit per bit. reset
definisce i limiti di continuazione e shift
prende una continuazione dallo stack di chiamate.
Appunti)
(*) In Scala il seguito finisce dove finisce il reset
blocco. 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 reset
blocco.
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 shift
ha 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