Продолжение Scala через содержательные примеры
Давайте определим from0to10
, который выражает идею итерации от 0 до 10:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
Теперь,
reset {
val x = from0to10()
print(s"$x ")
}
println()
печатает:
0 1 2 3 4 5 6 7 8 9 10
На самом деле x
нам не нужно:
reset {
print(s"${from0to10()} ")
}
println()
выводит тот же результат.
А также
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
печатает все пары:
(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)
Теперь, как это работает?
Это вызываемый код, from0to10
и вызывающий код. В данном случае это блок, следующий за reset
. Один из параметров, передаваемых вызываемому коду, - это адрес возврата, показывающий, какая часть вызывающего кода еще не была выполнена (**). Эта часть вызывающего кода является продолжением. Вызываемый код может делать с этим параметром все, что решит: передавать ему управление, игнорировать или вызывать его несколько раз. Здесь from0to10
вызывает это продолжение для каждого целого числа в диапазоне 0..10.
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i) // call the continuation
}
}
Но где заканчивается продолжение? Это важно, потому что последний return
из продолжения возвращает управление вызываемому коду from0to10
. В Scala он заканчивается там, где заканчивается блок reset
(*).
Теперь мы видим, что продолжение объявлено как cont: Int => Unit
. Почему? Мы вызываем from0to10
как val x = from0to10()
, а Int
- это тип значения, которое переходит в x
. Unit
означает, что блок после reset
не должен возвращать никакого значения (иначе будет ошибка типа). В общем, существует 4 типа сигнатуры: ввод функции, ввод продолжения, результат продолжения, результат функции. Все четыре должны соответствовать контексту вызова.
Выше мы напечатали пары значений. Распечатаем таблицу умножения. Но как вывести \n
после каждой строки?
Функция back
позволяет нам указать, что нужно сделать, когда управление вернется, от продолжения до кода, который его вызвал.
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
сначала вызывает его продолжение, а затем выполняет действие.
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ") // printf-like formatted i*j
}
Он печатает:
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
Что ж, теперь пришло время для головоломок. Есть два вызова from0to10
. Какое продолжение для первых from0to10
? Он следует за вызовом from0to10
в двоичном коде, но в исходном коде также включает оператор присваивания val i =
. Он заканчивается там, где заканчивается reset
блок, но конец reset
блока не возвращает управление первому from0to10
. Конец блока reset
возвращает управление второму from0to10
, который, в свою очередь, в конечном итоге возвращает управление back
, и именно back
возвращает управление первому вызову from0to10
. Когда первый (да! 1-й!) from0to10
выходит, весь reset
блок выходит.
Такой метод возврата управления называется backtracking, это очень старый метод, известный по крайней мере со времен Prolog и AI-ориентированных производных Lisp.
Имена reset
и shift
неверны. Эти имена лучше оставить для побитовых операций. reset
определяет границы продолжения, а shift
принимает продолжение из стека вызовов.
Примечания)
(*) В Scala продолжение заканчивается там, где заканчивается блок reset
. Другой возможный подход - позволить ему заканчиваться там, где заканчивается функция.
(**) Одним из параметров вызываемого кода является адрес возврата, который показывает, какая часть вызывающего кода еще не была выполнена. Ну, в Scala последовательность адресов возврата используется для что. Как много? Все адреса возврата помещаются в стек вызовов с момента входа в блок reset
.
UPD Часть 2 Отказ от продолжений: фильтрация
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont() // call continuation only for even numbers
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Это печатает:
0 2 4 6 8 10
Выделим две важные операции: отказ от продолжения (fail()
) и передача ему управления (succ()
):
// fail: just discard the continuation, force control to return back
def fail() = shift { (cont: Unit => Unit) => }
// succ: does nothing (well, passes control to the continuation), but has a funny signature
def succ():Unit @cpsParam[Unit,Unit] = { }
// def succ() = shift { (cont: Unit => Unit) => cont() }
Обе версии succ()
(см. Выше) работают. Оказывается, у shift
забавная подпись, и хотя succ()
ничего не делает, она должна иметь эту подпись для баланса типов.
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
как и ожидалось, он печатает
0 2 4 6 8 10
Внутри функции succ()
не требуется:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
снова он печатает
0 2 4 6 8 10
Теперь давайте определим onOdd()
через onEven()
:
// negation: the hard way
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException() // return is not allowed here
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
Выше, если x
четно, генерируется исключение и продолжение не вызывается; если x
нечетное, исключение не генерируется и вызывается продолжение. Приведенный выше код печатает:
1 3 5 7 9
person
18446744073709551615
schedule
19.12.2019