Acabo de terminar de programar en Scala y he estado investigando los cambios entre Scala 2.7 y 2.8. El que parece ser el más importante es el complemento de continuaciones, pero no entiendo para qué es útil ni cómo funciona. He visto que es bueno para la E / S asíncrona, pero no he podido averiguar por qué. Algunos de los recursos más populares sobre el tema son estos:
- Continuaciones delimitadas y Scala
- Ir a Scala
- A Taste of 2.8: Continuaciones
- Explicación de las continuaciones delimitadas (en Scala)
Y esta pregunta en Stack Overflow:
Desafortunadamente, ninguna de estas referencias intenta definir para qué son las continuaciones o qué se supone que deben hacer las funciones de cambio / reinicio, y no he encontrado ninguna referencia que lo haga. No he podido adivinar cómo funciona ninguno de los ejemplos en los artículos vinculados (o qué hacen), por lo que una forma de ayudarme podría ser ir línea por línea a través de una de esas muestras. Incluso este simple del tercer artículo:
reset {
...
shift { k: (Int=>Int) => // The continuation k will be the '_ + 1' below.
k(7)
} + 1
}
// Result: 8
¿Por qué el resultado es 8? Eso probablemente me ayudaría a empezar.

Respuestas:
Mi blog explica qué
resetyshifthacer, por lo que es posible que desee volver a leerlo.Otra buena fuente, que también señalo en mi blog, es la entrada de Wikipedia sobre estilo de paso de continuación . Ese es, con mucho, el más claro sobre el tema, aunque no usa la sintaxis de Scala, y la continuación se pasa explícitamente.
El artículo sobre las continuaciones delimitadas, al que enlazo en mi blog pero parece haberse roto, ofrece muchos ejemplos de uso.
Pero creo que el mejor ejemplo del concepto de continuaciones delimitadas es Scala Swarm. En ella, la biblioteca detiene la ejecución de su código en un punto, y el cálculo restante se convierte en la continuación. Luego, la biblioteca hace algo, en este caso, transfiere el cálculo a otro host y devuelve el resultado (el valor de la variable a la que se accedió) al cálculo que se detuvo.
Ahora, ni siquiera comprendes el ejemplo simple en la página de Scala, así que lee mi blog. En él solo me preocupa explicar estos conceptos básicos, de por qué es el resultado
8.fuente
Encontré que las explicaciones existentes son menos efectivas para explicar el concepto de lo que esperaba. Espero que este sea claro (y correcto). No he usado continuaciones todavía.
Cuando
cfse llama a una función de continuación :shiftbloque y comienza de nuevo al final.cfes lo que elshiftbloque "evalúa" a medida que continúa la ejecución. esto puede ser diferente para cada llamada acfresetbloque (o hasta una llamada aresetsi no hay ningún bloque)resetbloque (o el parámetro toreset() si no hay bloque) es lo quecfdevuelvecfhasta el final delshiftbloque.resetbloque (¿o una llamada para restablecer?)Entonces, en este ejemplo, siga las letras de la A a la Z
reset { // A shift { cf: (Int=>Int) => // B val eleven = cf(10) // E println(eleven) val oneHundredOne = cf(100) // H println(oneHundredOne) oneHundredOne } // C execution continues here with the 10 as the context // F execution continues here with 100 + 1 // D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven // G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne } // IEsto imprime:
11 101fuente
println(oneHundredOne) }a, por ejemplo,println(oneHundredOne); oneHundredOne }.cannot compute type for CPS-transformed function resulterror,+1seguirá inmediatamente despuésoneHundredOne}. Los comentarios que residen actualmente entre ellos rompen la gramática de alguna manera.Dado el ejemplo canónico del trabajo de investigación para las continuaciones delimitadas de Scala, modificado ligeramente para que la entrada de la función
shiftreciba el nombrefy, por lo tanto, ya no sea anónima.def f(k: Int => Int): Int = k(k(k(7))) reset( shift(f) + 1 // replace from here down with `f(k)` and move to `k` ) * 2El complemento Scala transforma este ejemplo de manera que el cálculo (dentro del argumento de entrada de
reset) que comienza desde cada unoshifthasta la invocación deresetse reemplaza con la función (por ejemplof) entrada ashift.El cálculo reemplazado se desplaza (es decir, se mueve) a una función
k. La funciónfingresa la funciónk, dondekcontiene el cálculo reemplazado, laskentradasx: Inty el cálculo enkreemplazashift(f)porx.f(k) * 2 def k(x: Int): Int = x + 1Que tiene el mismo efecto que:
k(k(k(7))) * 2 def k(x: Int): Int = x + 1Tenga en cuenta que el tipo
Intde parámetro de entradax(es decir, la firma de tipo dek) fue dado por la firma de tipo del parámetro de entrada def.Otro ejemplo prestado con la abstracción conceptualmente equivalente,
reades decir, es la entrada de la función parashift:def read(callback: Byte => Unit): Unit = myCallback = callback reset { val byte = "byte" val byte1 = shift(read) // replace from here with `read(callback)` and move to `callback` println(byte + "1 = " + byte1) val byte2 = shift(read) // replace from here with `read(callback)` and move to `callback` println(byte + "2 = " + byte2) }Creo que esto se traduciría al equivalente lógico de:
val byte = "byte" read(callback) def callback(x: Byte): Unit { val byte1 = x println(byte + "1 = " + byte1) read(callback2) def callback2(x: Byte): Unit { val byte2 = x println(byte + "2 = " + byte1) } }Espero que esto aclare la abstracción común coherente que quedó un tanto confusa por la presentación previa de estos dos ejemplos. Por ejemplo, el primer ejemplo canónico se presentó en el trabajo de investigación como una función anónima, en lugar de mi nombre
f, por lo que no está claro para algunos lectores que era análoga a la forma abstractareaden el prestado segundo ejemplo.Así, las continuaciones delimitadas crean la ilusión de una inversión de control de "me llamas desde fuera
reset" a "te llamo desde dentroreset".Tenga en cuenta que el tipo de retorno de
fes, perokno es, necesario que sea el mismo que el tipo de retorno dereset, es decir,ftiene la libertad de declarar cualquier tipo de retornoksiempre quefdevuelva el mismo tipo quereset. Lo mismo ocurre conreadycapture(ver también másENVabajo).Las continuaciones delimitadas no invierten implícitamente el control del estado, por ejemplo,
readycallbackno son funciones puras. Por lo tanto, el llamador no puede crear expresiones referencialmente transparentes y, por lo tanto, no tiene control declarativo (también conocido como transparente) sobre la semántica imperativa deseada .Podemos lograr explícitamente funciones puras con continuaciones delimitadas.
def aread(env: ENV): Tuple2[Byte,ENV] { def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback) shift(read) } def pure(val env: ENV): ENV { reset { val (byte1, env) = aread(env) val env = env.println("byte1 = " + byte1) val (byte2, env) = aread(env) val env = env.println("byte2 = " + byte2) } }Creo que esto se traduciría al equivalente lógico de:
def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV = env.myCallback(callback) def pure(val env: ENV): ENV { read(callback,env) def callback(x: Tuple2[Byte,ENV]): ENV { val (byte1, env) = x val env = env.println("byte1 = " + byte1) read(callback2,env) def callback2(x: Tuple2[Byte,ENV]): ENV { val (byte2, env) = x val env = env.println("byte2 = " + byte2) } } }Esto se está volviendo ruidoso, debido al entorno explícito.
Tenga en cuenta tangencialmente, Scala no tiene la inferencia de tipo global de Haskell y, por lo que yo sé, no podría soportar el levantamiento implícito a una mónada de estado
unit(como una posible estrategia para ocultar el entorno explícito), porque la inferencia de tipo global de Haskell (Hindley-Milner) depende de no admitir la herencia virtual múltiple de diamantes .fuente
reset/shiftse cambie adelimit/replace. Y por convención, quefyreadserwith, ykycallbackserreplaced,captured,continuation, ocallback.replacementlugar dewith. Afaik,()¿también está permitido? Afaik,{}es "la sintaxis ligera de Scala para cierres" , que oculta una llamada de función subyacente. Por ejemplo, vea cómo reescribí el de Danielsequence(tenga en cuenta que el código nunca fue compilado o probado, así que no dude en corregirme).shiftresetson funciones de biblioteca, no palabras clave. Por lo tanto,{}o()puede usarse cuando la función espera solo un parámetro . Scala tiene por nombre parámetros (véase la sección "9.5 Control de abstracciones" de la programación en Scala, segunda ed. Pág. 218), en la que si el parámetro es del tipo() => ...la() =>puede eliminar. SupongoUnity no por su nombre porque el bloque debería evaluarse antes de queresetse invoque, pero necesito{}varias declaraciones. Mi uso parashiftes correcto, porque obviamente ingresa un tipo de función.Continuación captura el estado de un cálculo, para ser invocado posteriormente.
Piense en el cálculo entre dejar la expresión de cambio y dejar la expresión de reinicio como una función. Dentro de la expresión de desplazamiento esta función se llama k, es la continuación. Puede pasarlo, invocarlo más tarde, incluso más de una vez.
Creo que el valor devuelto por la expresión de restablecimiento es el valor de la expresión dentro de la expresión de cambio después de =>, pero no estoy muy seguro de esto.
Entonces, con continuaciones, puede envolver una pieza de código bastante arbitraria y no local en una función. Esto se puede utilizar para implementar un flujo de control no estándar, como corrutina o retroceso.
Entonces, las continuaciones deben usarse a nivel de sistema. Rociarlos a través del código de su aplicación sería una receta segura para las pesadillas, mucho peor de lo que podría ser el peor código espagueti que usa goto.
Descargo de responsabilidad: no tengo una comprensión profunda de las continuaciones en Scala, solo lo infiero al mirar los ejemplos y conocer las continuaciones de Scheme.
fuente
Desde mi punto de vista, la mejor explicación se dio aquí: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html
Uno de los ejemplos:
reset { println("A") shift { k1: (Unit=>Unit) => println("B") k1() println("C") } println("D") shift { k2: (Unit=>Unit) => println("E") k2() println("F") } println("G") }A B D E G F Cfuente
Otro artículo (más reciente, mayo de 2016) sobre las continuaciones de Scala es:
" Viaje en el tiempo en Scala: CPS en Scala (continuación de scala) " por Shivansh Srivastava (
shiv4nsh) .También se refiere a Jim McBeath 's artículo mencionado en Dmitry Bespalov ' s respuesta .
Pero antes de eso, describe Continuaciones así:
Dicho esto, como se anunció en abril de 2014 para Scala 2.11.0-RC1
fuente
Continuaciones de Scala a través de ejemplos significativos
Definamos
from0to10que 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 10De 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 ,
from0to10y el código de llamada . En este caso, es el bloque que siguereset. 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ífrom0to10llama 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) // call the continuation } }Pero, ¿dónde termina la continuación? Esto es importante porque la última
returnde las declaraciones de continuación de control al código de llamada,from0to10. En Scala, termina donde termina elresetbloque (*).Ahora, vemos que la continuación se declara como
cont: Int => Unit. ¿Por qué? Invocamosfrom0to10comoval x = from0to10(), yIntes el tipo de valor al que vax.Unitsignifica que el bloque posterior noresetdebe 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
\ndespués de cada fila?La función
backnos 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 }backprimero 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 ") // printf-like formatted i*j }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 100Bueno, ahora es el momento de algunos acertijos. Hay dos invocaciones de
from0to10. ¿Cuál es la continuación del primerofrom0to10? Sigue a la invocación defrom0to10en el código binario , pero en el código fuente también incluye la declaración de asignaciónval i =. Termina donde termina elresetbloque, pero el final delresetbloque no devuelve el control al primerofrom0to10. El final delresetbloque devuelve el control al segundofrom0to10, que a su vez finalmente devuelve el control aback, y es elbackque devuelve el control a la primera invocación defrom0to10. Cuandofrom0to10sale la primera (¡sí! ¡1ra!) , Se sale de todo elresetbloque.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
resetyshiftson nombres inapropiados. Es mejor que estos nombres se hayan dejado para las operaciones bit a bit.resetdefine los límites de continuación yshifttoma una continuación de la pila de llamadas.Nota (s)
(*) En Scala, la continuación termina donde termina el
resetbloque. 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
resetbloque.UPD Part 2 Descartando Continuaciones: Filtrado
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 ") }Esto imprime:
0 2 4 6 8 10Consideremos dos operaciones importantes: descartar la continuación (
fail()) y pasarle el control (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() }Ambas versiones de
succ()(arriba) funcionan. Resulta queshifttiene una firma divertida, y aunquesucc()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 10Dentro 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 10Ahora, definamos a
onOdd()través deonEven():// 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 ") }Arriba, si
xes par, se lanza una excepción y no se llama a la continuación; sixes impar, no se lanza la excepción y se llama a la continuación. Se imprime el código anterior:1 3 5 7 9fuente