Kontynuacje Scali poprzez znaczące przykłady
Zdefiniujmy, from0to10co wyraża ideę iteracji od 0 do 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Teraz,
reset {
val x = from0to10()
print(s"$x ")
}
println()
wydruki:
0 1 2 3 4 5 6 7 8 9 10
W rzeczywistości nie potrzebujemy x:
reset {
print(s"${from0to10()} ")
}
println()
drukuje ten sam wynik.
I
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
drukuje wszystkie pary:
(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10)
Jak to działa?
Jest to nazywane kod , from0to10oraz kod wywołujący . W tym przypadku jest to następny blok reset. Jednym z parametrów przekazywanych do wywoływanego kodu jest adres zwrotny, który pokazuje, która część kodu wywołującego nie została jeszcze wykonana (**). Ta część kodu wywołującego jest kontynuacją . Wywołany kod może zrobić z tym parametrem wszystko, co zdecyduje: przekazać mu kontrolę, zignorować lub wywołać go wiele razy. Tutaj from0to10wywołuje tę kontynuację dla każdej liczby całkowitej z zakresu 0..10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Ale gdzie kończy się kontynuacja? Jest to ważne, ponieważ ostatni returnz powrotów do kontynuacji kontrolować zwanego kodu from0to10. W Scali kończy się tam, gdzie kończy się resetblok (*).
Teraz widzimy, że kontynuacja jest zadeklarowana jako cont: Int => Unit. Czemu? Wywołujemy from0to10jako val x = from0to10()i Intjest to typ wartości, do której trafia x. Unitoznacza, że blok po nie resetmoże zwracać żadnej wartości (w przeciwnym razie wystąpi błąd typu). Ogólnie rzecz biorąc, istnieją 4 rodzaje sygnatur: wejście funkcji, wejście kontynuacji, wynik kontynuacji, wynik funkcji. Wszystkie cztery muszą pasować do kontekstu wywołania.
Powyżej wydrukowaliśmy pary wartości. Wydrukujmy tabliczkę mnożenia. Ale jak wyprowadzamy dane \npo każdym wierszu?
Funkcja backpozwala nam określić, co należy zrobić, gdy sterowanie wróci, od kontynuacji do kodu, który ją wywołał.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
backnajpierw wywołuje jego kontynuację, a następnie wykonuje akcję .
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
Drukuje:
0 0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 10
0 2 4 6 8 10 12 14 16 18 20
0 3 6 9 12 15 18 21 24 27 30
0 4 8 12 16 20 24 28 32 36 40
0 5 10 15 20 25 30 35 40 45 50
0 6 12 18 24 30 36 42 48 54 60
0 7 14 21 28 35 42 49 56 63 70
0 8 16 24 32 40 48 56 64 72 80
0 9 18 27 36 45 54 63 72 81 90
0 10 20 30 40 50 60 70 80 90 100
Cóż, teraz czas na kilka łamigłówek. Istnieją dwa wywołania from0to10. Jaka jest kontynuacja pierwszego from0to10? Następuje po wywołaniu from0to10w kodzie binarnym , ale w kodzie źródłowym zawiera również instrukcję przypisania val i =. Kończy się tam, gdzie kończy się resetblok, ale koniec resetbloku nie zwraca kontroli do pierwszego from0to10. Koniec resetzwrotu bloku sterowania do 2 from0to10, które z kolei ostatecznie zwraca kontrolę back, a to back, że sterowanie powraca do pierwszej pw from0to10. Kiedy pierwszy (tak! Pierwszy!) from0to10Wyjdzie, następuje wyjście z całego resetbloku.
Taki sposób przywracania kontroli nazywa się cofaniem , jest to bardzo stara technika, znana przynajmniej z czasów Prologu i zorientowanych na AI pochodnych Lispa.
Nazwy reseti shiftsą błędne. Te nazwy lepiej pozostawić dla operacji bitowych. resetdefiniuje granice kontynuacji i shiftpobiera kontynuację ze stosu wywołań.
Uwagi
(*) W Scali kontynuacja kończy się tam, gdzie kończy się resetblok. Innym możliwym podejściem byłoby pozostawienie jej końca tam, gdzie kończy się funkcja.
(**) Jednym z parametrów wywoływanego kodu jest adres zwrotny, który pokazuje, która część kodu wywołującego nie została jeszcze wykonana. Cóż, w Scali używa się do tego sekwencji adresów zwrotnych. Ile? Wszystkie adresy zwrotne umieszczone na stosie wywołań od momentu wejścia do resetbloku.
UPD Część 2
Odrzucanie Kontynuacji: Filtrowanie
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
To drukuje:
0 2 4 6 8 10
Rozważmy dwie ważne operacje: odrzucenie kontynuacji ( fail()) i przekazanie jej kontroli ( succ()):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Obie wersje succ()(powyżej) działają. Okazuje się, że shiftma zabawny podpis i chociaż succ()nic nie robi, to musi mieć ten podpis dla balansu typu.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
zgodnie z oczekiwaniami, drukuje
0 2 4 6 8 10
W ramach funkcji succ()nie jest konieczne:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
znowu drukuje
0 2 4 6 8 10
Teraz zdefiniujmy onOdd()przez onEven():
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException()
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
Powyżej, jeśli xjest równe, zgłaszany jest wyjątek, a kontynuacja nie jest wywoływana; jeśli xjest nieparzyste, wyjątek nie jest zgłaszany i wywoływana jest kontynuacja. Powyższy kod drukuje:
1 3 5 7 9