Поиск…


Вступление

Стиль продолжения передачи - это форма потока управления, которая включает в себя передачу функций остальной части вычисления в качестве аргумента «продолжение». Следующая функция вызывает это продолжение, чтобы продолжить выполнение программы. Один из способов думать о продолжении - это закрытие. Библиотека продолжения 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 может фактически использовать асинхронную библиотеку, в то время как ее код выглядит как нормальный императивный код.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow