Scala Language
Библиотека непрерывности
Поиск…
Вступление
Стиль продолжения передачи - это форма потока управления, которая включает в себя передачу функций остальной части вычисления в качестве аргумента «продолжение». Следующая функция вызывает это продолжение, чтобы продолжить выполнение программы. Один из способов думать о продолжении - это закрытие. Библиотека продолжения Scala приносит разграниченные продолжения в виде shift
/ reset
примитивов к языку.
библиотека продолжений: https://github.com/scala/scala-continuations
Синтаксис
- reset {...} // Продолжение продолжается до конца блока сброса
- shift {...} // Создаем продолжение, указывающее после вызова, передавая его закрытию
- A @cpsParam [B, C] // Вычисление, требующее функции A => B для создания значения C
- @cps [A] // Псевдоним для @cpsParam [A, A]
- @suspendable // Псевдоним для @cpsParam [Unit, Unit]
замечания
shift
и reset
- это примитивные структуры потока управления, такие как Int.+
- примитивная операция, а Long
- примитивный тип. Они более примитивны, чем либо в том, что разграниченные продолжения могут фактически использоваться для построения почти всех структур управления потоком. Они не очень полезны «из коробки», но они действительно блестят, когда они используются в библиотеках для создания богатых API.
Продолжения и монады также тесно связаны. В продолжение монады можно продолжить , и монады являются продолжениями, потому что их операция flatMap
берет продолжение как параметр.
Обратные вызовы - это продолжение
// 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)
}}
}}
Аргумент функции readFile
является продолжением, поскольку readFile
вызывает его, чтобы продолжить выполнение программы после того, как она выполнила свою работу.
Чтобы обуздать то, что может легко стать адским обратным вызовом, мы используем библиотеку продолжений.
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!
Создание функций, обеспечивающих непрерывность
Если shift
вызывается за пределами разделительного блока reset
, его можно использовать для создания функций, которые сами создают продолжения внутри блока reset
. Важно отметить, что тип shift
не просто (((A => B) => C) => A)
, он фактически (((A => B) => C) => (A @cpsParam[B, C]))
. Эти аннотации означают, что необходимы преобразования CPS. Функции, которые вызывают shift
без reset
имеют тип возвращаемого типа «заражен» этой аннотацией.
Внутри блока reset
значение A @cpsParam[B, C]
похоже имеет значение A
, хотя на самом деле это просто притворство. Продолжение, необходимое для завершения вычисления, имеет тип A => B
, поэтому код, следующий за методом, возвращающим этот тип, должен возвращать B
C
- это «реальный» тип возврата, а после преобразования CPS вызов функции имеет тип C
Теперь, пример, взятый из 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)
}
Здесь ask
будет хранить продолжение в карте, а затем некоторый другой код может получить этот «сеанс» и передать результат запроса пользователю. Таким образом, go
может фактически использовать асинхронную библиотеку, в то время как ее код выглядит как нормальный императивный код.