Szukaj…


Wprowadzenie

Styl przekazywania kontynuacji jest formą przepływu sterowania, który obejmuje przekazywanie do funkcji reszty obliczeń jako argument „kontynuacji”. Ta funkcja wywołuje później tę kontynuację, aby kontynuować wykonywanie programu. Jednym ze sposobów myślenia o kontynuacji jest zamknięcie. Biblioteka kontynuacji Scala zapewnia ograniczone kontynuacje w postaci shift / reset prymitywów do języka.

biblioteka kontynuacji: https://github.com/scala/scala-continuations

Składnia

  • reset {...} // Kontynuacje rozciągają się do końca otaczającego bloku resetowania
  • shift {...} // Stwórz kontynuację, poczynając od połączenia, przekazując go do zamknięcia
  • A @cpsParam [B, C] // Obliczenia, które wymagają funkcji A => B, aby utworzyć wartość C
  • @cps [A] // Alias dla @cpsParam [A, A]
  • @suspendable // Alias for @cpsParam [Unit, Unit]

Uwagi

shift i reset są prymitywnymi strukturami kontroli sterowania, jak Int.+ jest operacją prymitywną, a Long jest operacją prymitywną. Są bardziej prymitywne niż obie, ponieważ ograniczone ciągłości mogą być faktycznie wykorzystane do budowy prawie wszystkich struktur przepływu sterowania. Nie są bardzo przydatne „od razu po wyjęciu z pudełka”, ale naprawdę świecą, gdy są używane w bibliotekach do tworzenia bogatych interfejsów API.

Kontynuacje i monady są również ściśle powiązane. Kontynuacja może być kontynuowana w flatMap kontynuacyjnej , a monady są kontynuacjami, ponieważ ich operacja flatMap przyjmuje kontynuację jako parametr.

Oddzwanianie jest kontynuacją

// 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)
  }}
}}

Argument funkcji readFile jest kontynuacją, ponieważ readFile wywołuje ją, aby kontynuować wykonywanie programu po wykonaniu zadania.

Aby powstrzymać coś, co może łatwo stać się piekłem zwrotnym, używamy biblioteki kontynuacji.

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!

Tworzenie funkcji, które wymagają kontynuacji

Jeśli shift jest wywoływany poza ograniczającym blokiem reset , można go użyć do tworzenia funkcji, które same tworzą kontynuacje w bloku reset . Należy zauważyć, że typ shift to nie tylko (((A => B) => C) => A) , to w rzeczywistości (((A => B) => C) => (A @cpsParam[B, C])) . Ta adnotacja oznacza, gdzie potrzebne są transformacje CPS. Funkcje, które wywołują shift bez reset mają swój typ powrotu „zainfekowany” tą adnotacją.

Wewnątrz bloku reset wartość A @cpsParam[B, C] wydaje się mieć wartość A , chociaż tak naprawdę to tylko udawanie. Kontynuacja potrzebna do ukończenia obliczeń ma typ A => B , więc kod zgodny z metodą zwracającą ten typ musi zwrócić B C to „prawdziwy” typ zwracany, a po transformacji CPS wywołanie funkcji ma typ C

Teraz przykład zaczerpnięty ze Scaladoka z biblioteki

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)
}

W tym przypadku ask zapisze kontynuację w mapie, a później jakiś inny kod może pobrać tę „sesję” i przekazać wynik zapytania użytkownikowi. W ten sposób go może faktycznie używać biblioteki asynchronicznej, podczas gdy jej kod wygląda jak normalny kod rozkazujący.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow