Scala Language
Libreria di continuazioni
Ricerca…
introduzione
Lo stile di passaggio continuo è una forma di flusso di controllo che implica il passaggio alle funzioni del resto del calcolo come argomento di "continuazione". La funzione in questione richiama in seguito tale continuazione per continuare l'esecuzione del programma. Un modo per pensare a una continuazione è come una chiusura. La libreria di continuazioni Scala porta continuazioni delimitate nella forma dei primitivi che si shift
/ reset
al linguaggio.
libreria di continuazioni: https://github.com/scala/scala-continuations
Sintassi
- reset {...} // Le continuazioni si estendono fino alla fine del blocco di reset che lo racchiude
- shift {...} // Crea una continuazione che affermi dopo la chiamata, passandola alla chiusura
- A @cpsParam [B, C] // Un calcolo che richiede una funzione A => B per creare un valore di C
- @cps [A] // Alias per @cpsParam [A, A]
- @suspendable // Alias per @cpsParam [Unit, Unit]
Osservazioni
shift
e reset
sono strutture di flusso di controllo primitive, come Int.+
è un'operazione primitiva e Long
è un tipo primitivo. Sono più primitivi di quanto in quelle continuazioni delimitate possano essere effettivamente usati per costruire quasi tutte le strutture di flusso di controllo. Non sono molto utili "out-of-the-box", ma brillano davvero quando vengono utilizzati nelle librerie per creare API ricche.
Continuazioni e monadi sono anche strettamente collegate. Le continuazioni possono essere fatte nel monad di continuazione e le monadi sono continuazioni perché la loro operazione flatMap
prende una continuazione come parametro.
Le callback sono continui
// 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)
}}
}}
L'argomento della funzione su readFile
è una continuazione, in quanto readFile
richiama per continuare l'esecuzione del programma dopo che ha svolto il suo lavoro.
Al fine di contenere ciò che può facilmente diventare un inferno di callback, usiamo la libreria di continuazioni.
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!
Creazione di funzioni che portano a continuazione
Se lo shift
viene chiamato al di fuori di un blocco di reset
delimitante, può essere utilizzato per creare funzioni che creano continuazioni all'interno di un blocco di reset
. È importante notare che il tipo di shift
non è solo (((A => B) => C) => A)
, in realtà è (((A => B) => C) => (A @cpsParam[B, C]))
. Tale annotazione segna dove sono necessarie le trasformazioni CPS. Le funzioni che chiamano shift
senza reset
hanno il tipo di ritorno "infetto" con quell'annotazione.
All'interno di un blocco di reset
, il valore di A @cpsParam[B, C]
sembra avere un valore di A
, anche se in realtà è solo una finzione. La continuazione necessaria per completare il calcolo ha tipo A => B
, quindi il codice che segue un metodo che restituisce questo tipo deve restituire B
C
è il tipo di ritorno "reale", e dopo la trasformazione di CPS la chiamata di funzione ha il tipo C
Ora, l'esempio, preso dallo Scaladoc della 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)
}
Qui, ask
memorizzerà la continuazione in una mappa, e in seguito qualche altro codice può recuperare quella "sessione" e passare il risultato della query all'utente. In questo modo, go
può effettivamente utilizzare una libreria asincrona mentre il suo codice sembra un normale codice imperativo.