Scala Language
Fortsättningsbibliotek
Sök…
Introduktion
Fortsättningspasseringsstil är en form av kontrollflöde som innebär att överföra till att fungera resten av beräkningen som ett "fortsättning" -argument. Funktionen i fråga åberopar senare den fortsättningen för att fortsätta programutförandet. Ett sätt att tänka på en fortsättning är som en nedläggning. Scala-fortsättningsbiblioteket ger avgränsade fortsättningar i form av primitivens shift
/ reset
till språket.
fortsättningsbibliotek: https://github.com/scala/scala-continuations
Syntax
- återställ {...} // Fortsättningar sträcker sig upp till slutet av det bifogade återställningsblocket
- skift {...} // Skapa en fortsättning som anger från efter samtalet och vidarebefordras till avslutningen
- A @cpsParam [B, C] // En beräkning som kräver en funktion A => B för att skapa ett värde på C
- @cps [A] // Alias för @cpsParam [A, A]
- @suspendable // Alias för @cpsParam [Enhet, enhet]
Anmärkningar
shift
och reset
är primitiva kontrollflödesstrukturer, som Int.+
är en primitiv operation och Long
är en primitiv typ. De är mer primitiva än antingen genom att avgränsade fortsättningar faktiskt kan användas för att konstruera nästan alla kontrollflödesstrukturer. De är inte särskilt användbara "out-of-the-box", men de lyser verkligen när de används i bibliotek för att skapa rika API: er.
Fortsättningar och monader är också nära kopplade. Fortsättningar kan göras till fortsättningsmonaden och monader är fortsättningar eftersom deras flatMap
operation tar en fortsättning som parameter.
Återuppringning är kontinuerliga
// 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)
}}
}}
Funktionsargumentet för readFile
är en fortsättning, genom att readFile
åberopar det att fortsätta programutförandet efter att det har gjort sitt jobb.
För att täppa till det som lätt kan bli återuppringande helvete använder vi fortsättningsbiblioteket.
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!
Skapa funktioner som tar fortsättningar
Om shift
kallas utanför ett avgränsande reset
, kan det användas för att skapa funktioner som själva skapar fortsättningar i ett reset
. Det är viktigt att notera att shift
typ inte bara är (((A => B) => C) => A)
, det är faktiskt (((A => B) => C) => (A @cpsParam[B, C]))
. Den anteckningen markerar där CPS-transformationer behövs. Funktioner som ringer shift
utan reset
har sin returtyp "infekterad" med den anteckningen.
Inuti ett reset
verkar ett värde på A @cpsParam[B, C]
ha ett värde på A
, även om det egentligen bara låtsas. Den fortsättning som behövs för att slutföra beräkningen har typ A => B
, så koden som följer en metod som returnerar denna typ måste returnera B
C
är den "riktiga" returtypen, och efter CPS-transformation har funktionssamtalet typen C
Nu, exemplet, taget från bibliotekets Scaladoc
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)
}
Här kommer ask
att lagra fortsättningen på en karta, och senare kan någon annan kod hämta den "sessionen" och skicka resultatet av frågan till användaren. På det här sättet kan go
faktiskt använda ett asynkront bibliotek medan dess kod ser ut som normal nödvändig kod.