Scala Language
Vervolg Bibliotheek
Zoeken…
Invoering
De stijl voor het doorgaan van voortzetting is een vorm van besturingsstroom waarbij de rest van de berekening wordt doorgegeven aan functies als een "voortzetting" -argument. De functie in kwestie roept later die voortzetting op om de uitvoering van het programma voort te zetten. Een manier om een voortzetting te beschouwen is als een sluiting. De Scala voortzettingen bibliotheek brengt afgebakend voortzettingen in de vorm van de primitieven shift
/ reset
om de taal.
vervolgbibliotheek: https://github.com/scala/scala-continuations
Syntaxis
- reset {...} // Continuaties lopen door tot het einde van het omringende resetblok
- shift {...} // Creëer een vervolg met vermelding van na het gesprek en geef dit door aan de afsluiting
- A @cpsParam [B, C] // Een berekening die een functie A => B vereist om een waarde van C te creëren
- @cps [A] // Alias voor @cpsParam [A, A]
- @suspendable // Alias voor @cpsParam [Eenheid, Eenheid]
Opmerkingen
shift
en reset
zijn primitieve besturingsstroomstructuren, zoals Int.+
is een primitieve bewerking en Long
is een primitief type. Ze zijn primitiever dan in beide dat afgebakende voortzettingen feitelijk kunnen worden gebruikt om bijna alle besturingsstroomstructuren te construeren. Ze zijn niet erg handig "out-of-the-box", maar ze schitteren echt wanneer ze in bibliotheken worden gebruikt om rijke API's te maken.
Continuaties en monaden zijn ook nauw met elkaar verbonden. Voortzettingen kunnen worden gemaakt in de voortzettingsmonade , en monaden zijn voortzettingen omdat hun flatMap
bewerking een voortzetting als parameter neemt.
Terugbellen zijn continuaties
// 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)
}}
}}
Het functieargument voor readFile
is een voortzetting, in die zin dat readFile
aanroept om de programma-uitvoering voort te zetten nadat het zijn werk heeft gedaan.
Om te beheersen wat gemakkelijk callback-hel kan worden, gebruiken we de vervolgbibliotheek.
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!
Functies creëren die doorgaan
Als shift
buiten een begrenzende heet reset
blok, kan het worden gebruikt om functies die zichzelf creëren voortzettingen in het creëren van een reset
blok. Het is belangrijk op te merken dat het type van shift
niet alleen (((A => B) => C) => A)
, maar eigenlijk (((A => B) => C) => (A @cpsParam[B, C]))
. Die annotatie geeft aan waar CPS-transformaties nodig zijn. Functies die shift
oproepen zonder reset
hebben hun retourtype "besmet" met die annotatie.
Binnen een reset
blok, een waarde van A @cpsParam[B, C]
lijkt een waarde hebben van A
, maar eigenlijk is het gewoon doen alsof. De voortzetting die nodig is om de berekening te voltooien, heeft type A => B
, dus de code die een methode volgt die dit type retourneert, moet B
retourneren. C
is het "echte" retourtype en na CPS-transformatie heeft de functieaanroep het type C
Nu het voorbeeld, genomen uit de Scaladoc van de bibliotheek
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)
}
Hier zal ask
de voortzetting opslaan in een kaart, en later kan een andere code die "sessie" ophalen en het resultaat van de query doorgeven aan de gebruiker. Op deze manier kan go
een asynchrone bibliotheek gebruiken, terwijl de code eruitziet als normale imperatieve code.