Kontynuacje Scali poprzez znaczące przykłady
Zdefiniujmy, from0to10
co 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 , from0to10
oraz 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 from0to10
wywoł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 return
z powrotów do kontynuacji kontrolować zwanego kodu from0to10
. W Scali kończy się tam, gdzie kończy się reset
blok (*).
Teraz widzimy, że kontynuacja jest zadeklarowana jako cont: Int => Unit
. Czemu? Wywołujemy from0to10
jako val x = from0to10()
i Int
jest to typ wartości, do której trafia x
. Unit
oznacza, że blok po nie reset
moż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 \n
po każdym wierszu?
Funkcja back
pozwala 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
}
back
najpierw 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 from0to10
w kodzie binarnym , ale w kodzie źródłowym zawiera również instrukcję przypisania val i =
. Kończy się tam, gdzie kończy się reset
blok, ale koniec reset
bloku nie zwraca kontroli do pierwszego from0to10
. Koniec reset
zwrotu 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!) from0to10
Wyjdzie, następuje wyjście z całego reset
bloku.
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 reset
i shift
są błędne. Te nazwy lepiej pozostawić dla operacji bitowych. reset
definiuje granice kontynuacji i shift
pobiera kontynuację ze stosu wywołań.
Uwagi
(*) W Scali kontynuacja kończy się tam, gdzie kończy się reset
blok. 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 reset
bloku.
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 shift
ma 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 x
jest równe, zgłaszany jest wyjątek, a kontynuacja nie jest wywoływana; jeśli x
jest nieparzyste, wyjątek nie jest zgłaszany i wywoływana jest kontynuacja. Powyższy kod drukuje:
1 3 5 7 9