Continuaciones de Scala a través de ejemplos significativos
Definamos from0to10
que expresa la idea de iteración de 0 a 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Ahora,
reset {
val x = from0to10()
print(s"$x ")
}
println()
huellas dactilares:
0 1 2 3 4 5 6 7 8 9 10
De hecho, no necesitamos x
:
reset {
print(s"${from0to10()} ")
}
println()
imprime el mismo resultado.
Y
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
imprime todos los pares:
(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)
Ahora, ¿cómo funciona eso?
No es el código de llamada , from0to10
y el código de llamada . En este caso, es el bloque que sigue reset
. Uno de los parámetros pasados al código llamado es una dirección de retorno que muestra qué parte del código de llamada aún no se ha ejecutado (**). Esa parte del código de llamada es la continuación . El código llamado puede hacer con ese parámetro lo que decida: pasarle el control, ignorarlo o llamarlo varias veces. Aquí from0to10
llama a esa continuación para cada número entero en el rango 0..10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Pero, ¿dónde termina la continuación? Esto es importante porque la última return
de las declaraciones de continuación de control al código de llamada, from0to10
. En Scala, termina donde termina el reset
bloque (*).
Ahora, vemos que la continuación se declara como cont: Int => Unit
. ¿Por qué? Invocamos from0to10
como val x = from0to10()
, y Int
es el tipo de valor al que va x
. Unit
significa que el bloque posterior no reset
debe devolver ningún valor (de lo contrario, habrá un error de tipo). En general, hay 4 firmas de tipo: entrada de función, entrada de continuación, resultado de continuación, resultado de función. Los cuatro deben coincidir con el contexto de invocación.
Arriba, imprimimos pares de valores. Imprimamos la tabla de multiplicar. Pero, ¿cómo salimos \n
después de cada fila?
La función back
nos permite especificar qué se debe hacer cuando el control regrese, desde la continuación hasta el código que lo llamó.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
primero llama a su continuación y luego realiza la acción .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
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
Bueno, ahora es el momento de algunos acertijos. Hay dos invocaciones de from0to10
. ¿Cuál es la continuación del primero from0to10
? Sigue a la invocación de from0to10
en el código binario , pero en el código fuente también incluye la declaración de asignación val i =
. Termina donde termina el reset
bloque, pero el final del reset
bloque no devuelve el control al primero from0to10
. El final del reset
bloque devuelve el control al segundo from0to10
, que a su vez finalmente devuelve el control a back
, y es el back
que devuelve el control a la primera invocación de from0to10
. Cuando from0to10
sale la primera (¡sí! ¡1ra!) , Se sale de todo el reset
bloque.
Este método de devolver el control se llama retroceso , es una técnica muy antigua, conocida al menos desde los tiempos de Prolog y los derivados Lisp orientados a AI.
Los nombres reset
y shift
son nombres inapropiados. Es mejor que estos nombres se hayan dejado para las operaciones bit a bit. reset
define los límites de continuación y shift
toma una continuación de la pila de llamadas.
Nota (s)
(*) En Scala, la continuación termina donde termina el reset
bloque. Otro enfoque posible sería dejar que termine donde termina la función.
(**) Uno de los parámetros del código llamado es una dirección de retorno que muestra qué parte del código de llamada aún no se ha ejecutado. Bueno, en Scala, se usa una secuencia de direcciones de retorno para eso. ¿Cuántos? Todas las direcciones de retorno colocadas en la pila de llamadas desde que ingresaron al reset
bloque.
UPD Part 2
Descartando Continuaciones: Filtrado
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Esto imprime:
0 2 4 6 8 10
Consideremos dos operaciones importantes: descartar la continuación ( fail()
) y pasarle el control ( succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Ambas versiones de succ()
(arriba) funcionan. Resulta que shift
tiene una firma divertida, y aunque succ()
no hace nada, debe tener esa firma para el balance de tipo.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
como se esperaba, imprime
0 2 4 6 8 10
Dentro de una función, succ()
no es necesario:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
de nuevo, imprime
0 2 4 6 8 10
Ahora, definamos a onOdd()
través de 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 ")
}
Arriba, si x
es par, se lanza una excepción y no se llama a la continuación; si x
es impar, no se lanza la excepción y se llama a la continuación. Se imprime el código anterior:
1 3 5 7 9