Scala Language
Biblioteca de continuaciones
Buscar..
Introducción
El estilo de paso de continuación es una forma de flujo de control que implica pasar a las funciones el resto de la computación como un argumento de "continuación". La función en cuestión luego invoca esa continuación para continuar la ejecución del programa. Una forma de pensar en una continuación es como un cierre. La biblioteca de continuaciones de Scala trae continuaciones delimitadas en la forma de los shift
primitivos / reset
al lenguaje.
biblioteca de continuaciones: https://github.com/scala/scala-continuations
Sintaxis
- reset {...} // Las continuaciones se extienden hasta el final del bloque de reinicio adjunto
- shift {...} // Crear una continuación indicando después de la llamada, pasándola al cierre
- A @cpsParam [B, C] // Un cálculo que requiere una función A => B para crear un valor de C
- @cps [A] // Alias para @cpsParam [A, A]
- @suspendable // Alias para @cpsParam [Unidad, Unidad]
Observaciones
shift
y reset
son estructuras de flujo de control primitivas, como Int.+
es una operación primitiva y Long
es un tipo primitivo. Son más primitivos que cualquiera de los dos en que las continuaciones delimitadas se pueden usar para construir casi todas las estructuras de flujo de control. No son muy útiles "listos para usar", pero realmente brillan cuando se usan en bibliotecas para crear API ricas.
Las continuaciones y las mónadas también están estrechamente vinculadas. Las continuaciones se pueden hacer en la mónada de continuación , y las mónadas son continuaciones porque su operación de flatMap
toma una continuación como parámetro.
Las devoluciones de llamada son continuaciones
// Takes a callback and executes it with the read value
def readFile(path: String)(callback: Try[String] => Unit): Unit = ???
readFile(path) { _.flatMap { file1 =>
readFile(path2) { _.foreach { file2 =>
processFiles(file1, file2)
}}
}}
El argumento de la función para readFile
es una continuación, en que readFile
invoca para continuar la ejecución del programa después de que haya realizado su trabajo.
Para controlar lo que puede convertirse fácilmente en un infierno de devolución de llamada, usamos la biblioteca de continuaciones.
reset { // Reset is a delimiter for continuations.
for { // Since the callback hell is relegated to continuation library machinery.
// a for-comprehension can be used
file1 <- shift(readFile(path1)) // shift has type (((A => B) => C) => A)
// We use it as (((Try[String] => Unit) => Unit) => Try[String])
// It takes all the code that occurs after it is called, up to the end of reset, and
// makes it into a closure of type (A => B).
// The reason this works is that shift is actually faking its return type.
// It only pretends to return A.
// It actually passes that closure into its function parameter (readFile(path1) here),
// And that function calls what it thinks is a normal callback with an A.
// And through compiler magic shift "injects" that A into its own callsite.
// So if readFile calls its callback with parameter Success("OK"),
// the shift is replaced with that value and the code is executed until the end of reset,
// and the return value of that is what the callback in readFile returns.
// If readFile called its callback twice, then the shift would run this code twice too.
// Since readFile returns Unit though, the type of the entire reset expression is Unit
//
// Think of shift as shifting all the code after it into a closure,
// and reset as resetting all those shifts and ending the closures.
file2 <- shift(readFile(path2))
} processFiles(file1, file2)
}
// After compilation, shift and reset are transformed back into closures
// The for comprehension first desugars to:
reset {
shift(readFile(path1)).flatMap { file1 => shift(readFile(path2)).foreach { file2 => processFiles(file1, file2) } }
}
// And then the callbacks are restored via CPS transformation
readFile(path1) { _.flatMap { file1 => // We see how shift moves the code after it into a closure
readFile(path2) { _.foreach { file2 =>
processFiles(file1, file2)
}}
}} // And we see how reset closes all those closures
// And it looks just like the old version!
Creando funciones que toman continuaciones
Si se llama shift
fuera de un bloque de reset
delimitador, se puede usar para crear funciones que crean continuaciones dentro de un bloque de reset
. Es importante tener en cuenta que el tipo de shift
no es solo (((A => B) => C) => A)
, en realidad es (((A => B) => C) => (A @cpsParam[B, C]))
. Esa anotación marca dónde se necesitan las transformaciones de CPS. Las funciones que llaman a shift
sin reset
tienen su tipo de retorno "infectado" con esa anotación.
Dentro de un bloque de reset
, un valor de A @cpsParam[B, C]
parece tener un valor de A
, aunque en realidad es solo una simulación. La continuación que se necesita para completar el cálculo tiene el tipo A => B
, por lo que el código que sigue un método que devuelve este tipo debe devolver B
C
es el tipo de retorno "real" y, después de la transformación de CPS, la llamada a la función tiene el tipo C
Ahora, el ejemplo, tomado del Scaladoc de la biblioteca.
val sessions = new HashMap[UUID, Int=>Unit]
def ask(prompt: String): Int @suspendable = // alias for @cpsParam[Unit, Unit]. @cps[Unit] is also an alias. (@cps[A] = @cpsParam[A,A])
shift {
k: (Int => Unit) => {
println(prompt)
val id = uuidGen
sessions += id -> k
}
}
def go(): Unit = reset {
println("Welcome!")
val first = ask("Please give me a number") // Uses CPS just like shift
val second = ask("Please enter another number")
printf("The sum of your numbers is: %d\n", first + second)
}
Aquí, ask
almacenará la continuación en un mapa, y luego otro código puede recuperar esa "sesión" y pasar el resultado de la consulta al usuario. De esta manera, go
puede estar utilizando una biblioteca asíncrona mientras su código parece un código imperativo normal.