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é
reset
yshift
hacer, 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
cf
se llama a una función de continuación :shift
bloque y comienza de nuevo al final.cf
es lo que elshift
bloque "evalúa" a medida que continúa la ejecución. esto puede ser diferente para cada llamada acf
reset
bloque (o hasta una llamada areset
si no hay ningún bloque)reset
bloque (o el parámetro toreset
() si no hay bloque) es lo quecf
devuelvecf
hasta el final delshift
bloque.reset
bloque (¿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 } // I
Esto imprime:
11 101
fuente
println(oneHundredOne) }
a, por ejemplo,println(oneHundredOne); oneHundredOne }
.cannot compute type for CPS-transformed function result
error,+1
seguirá 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
shift
reciba el nombref
y, 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` ) * 2
El complemento Scala transforma este ejemplo de manera que el cálculo (dentro del argumento de entrada de
reset
) que comienza desde cada unoshift
hasta la invocación dereset
se 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ónf
ingresa la funciónk
, dondek
contiene el cálculo reemplazado, lask
entradasx: Int
y el cálculo enk
reemplazashift(f)
porx
.f(k) * 2 def k(x: Int): Int = x + 1
Que tiene el mismo efecto que:
k(k(k(7))) * 2 def k(x: Int): Int = x + 1
Tenga en cuenta que el tipo
Int
de 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,
read
es 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 abstractaread
en 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
f
es, perok
no es, necesario que sea el mismo que el tipo de retorno dereset
, es decir,f
tiene la libertad de declarar cualquier tipo de retornok
siempre quef
devuelva el mismo tipo quereset
. Lo mismo ocurre conread
ycapture
(ver también másENV
abajo).Las continuaciones delimitadas no invierten implícitamente el control del estado, por ejemplo,
read
ycallback
no 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
/shift
se cambie adelimit
/replace
. Y por convención, quef
yread
serwith
, yk
ycallback
serreplaced
,captured
,continuation
, ocallback
.replacement
lugar 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).shift
reset
son 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. SupongoUnit
y no por su nombre porque el bloque debería evaluarse antes de quereset
se invoque, pero necesito{}
varias declaraciones. Mi uso parashift
es 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 C
fuente
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
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 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í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) // call the continuation } }
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 elreset
bloque (*).Ahora, vemos que la continuación se declara como
cont: Int => Unit
. ¿Por qué? Invocamosfrom0to10
comoval x = from0to10()
, yInt
es el tipo de valor al que vax
.Unit
significa que el bloque posterior noreset
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 ") // 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 100
Bueno, 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 defrom0to10
en el código binario , pero en el código fuente también incluye la declaración de asignaciónval i =
. Termina donde termina elreset
bloque, pero el final delreset
bloque no devuelve el control al primerofrom0to10
. El final delreset
bloque devuelve el control al segundofrom0to10
, que a su vez finalmente devuelve el control aback
, y es elback
que devuelve el control a la primera invocación defrom0to10
. Cuandofrom0to10
sale la primera (¡sí! ¡1ra!) , Se sale de todo elreset
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
yshift
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 yshift
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() // call continuation only for even numbers } } 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()
):// 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 queshift
tiene 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 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 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
x
es par, se lanza una excepción y no se llama a la continuación; six
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
fuente