Continuations de Scala via des exemples significatifs
Définissons ce from0to10
qui exprime l'idée d'itération de 0 à 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Maintenant,
reset {
val x = from0to10()
print(s"$x ")
}
println()
imprime:
0 1 2 3 4 5 6 7 8 9 10
En fait, nous n'avons pas besoin x
:
reset {
print(s"${from0to10()} ")
}
println()
imprime le même résultat.
Et
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
imprime toutes les paires:
(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)
Maintenant, comment ça marche?
Il y a le code appelé , from0to10
et le code d'appel . Dans ce cas, c'est le bloc qui suit reset
. L'un des paramètres passés au code appelé est une adresse de retour qui montre quelle partie du code appelant n'a pas encore été exécutée (**). Cette partie du code d'appel est la suite . Le code appelé peut faire avec ce paramètre tout ce qu'il décide de faire: lui passer le contrôle, ou l'ignorer, ou l'appeler plusieurs fois. Ici from0to10
appelle cette continuation pour chaque entier dans la plage 0..10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Mais où finit la suite? Ceci est important parce que la dernière return
des déclarations de continuation le contrôle au code appelé, from0to10
. Dans Scala, il se termine là où lereset
bloc se termine (*).
Maintenant, nous voyons que la suite est déclarée comme cont: Int => Unit
. Pourquoi? Nous invoquons from0to10
as val x = from0to10()
, et Int
est le type de valeur qui va vers x
. Unit
signifie que le bloc aprèsreset
doit renvoyer aucune valeur (sinon il y aura une erreur de type). En général, il existe 4 signatures de type: entrée de fonction, entrée de continuation, résultat de continuation, résultat de fonction. Tous les quatre doivent correspondre au contexte d'appel.
Ci-dessus, nous avons imprimé des paires de valeurs. Imprimons la table de multiplication. Mais comment sortons-nous\n
après chaque ligne?
La fonction back
nous permet de spécifier ce qui doit être fait lorsque le contrôle revient, de la suite au code qui l'a appelé.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
appelle d'abord sa continuation, puis exécute l' action .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
Il imprime:
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
Eh bien, maintenant il est temps pour certains cerveaux. Il existe deux invocations de from0to10
. Quelle est la suite pour le premier from0to10
? Il suit l'appel de from0to10
dans le code binaire , mais dans le code source, il comprend également l'instruction d'affectation val i =
. Il se termine là où le reset
bloc se termine, mais la fin du reset
bloc ne renvoie pas le contrôle au premier from0to10
. La fin du reset
bloc renvoie le contrôle au 2nd from0to10
, qui à son tour retourne finalement le contrôle à back
, et c'est cela back
qui renvoie le contrôle au premier appel de from0to10
. Quand la première (oui! La 1ère!) from0to10
Sort, l'ensemblereset
bloc est quitté.
Une telle méthode de retour de contrôle est appelée retour arrière , c'est une technique très ancienne, connue au moins depuis l'époque des dérivés Lisp orientés Prolog et AI.
Les noms reset
et shift
sont des noms erronés. Ces noms auraient mieux dû être laissés pour les opérations au niveau du bit. reset
définit les limites de continuation et shift
prend une continuation de la pile d'appels.
Remarques)
(*) Dans Scala, la suite se termine là où le reset
bloc se termine.Une autre approche possible serait de le laisser se terminer là où la fonction se termine.
(**) L'un des paramètres du code appelé est une adresse de retour qui montre quelle partie du code appelant n'a pas encore été exécutée. Eh bien, dans Scala, une séquence d'adresses de retour est utilisée pour cela. Combien? Toutes les adresses de retour placées sur la pile d'appels depuis l'entrée dans le reset
bloc.
UPD Partie 2
Annulation des continuations: filtrage
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Cela imprime:
0 2 4 6 8 10
Retenons deux opérations importantes: abandonner la continuation ( fail()
) et lui passer le contrôle ( succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Les deux versions de succ()
(ci-dessus) fonctionnent. Il s'avère que cela shift
a une signature amusante, et bien que succ()
ne fasse rien, il doit avoir cette signature pour l'équilibre de type.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
comme prévu, il imprime
0 2 4 6 8 10
Dans une fonction, succ()
n'est pas nécessaire:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
encore une fois, il imprime
0 2 4 6 8 10
Maintenant, définissons onOdd()
via 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 ")
}
Au-dessus, si x
est pair, une exception est levée et la continuation n'est pas appelée; si x
est impair, l'exception n'est pas levée et la suite est appelée. Le code ci-dessus s'imprime:
1 3 5 7 9