Продолжение 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)
}
}
Но где кончается продолжение? Это важно, потому что последний return
из продолжения возвращает управление вызываемому коду from0to10
. В Scala он заканчивается там, где reset
заканчивается блок (*).
Теперь мы видим, что продолжение объявлено как cont: Int => Unit
. Зачем? Мы вызываем from0to10
as 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 ")
}
Он печатает:
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
блока возвращает управление 2-му from0to10
, который, в свою очередь, в конечном итоге возвращает управление back
, и именно он back
возвращает управление первому вызову from0to10
. Когда первый (да! 1-й!) from0to10
Выходит, весь reset
блок выходит.
Такой метод возврата управления называется backtracking , это очень старая техника, известная по крайней мере со времен Prolog и AI-ориентированных производных Lisp.
Имена reset
и shift
употребляются неправильно. Эти имена лучше оставить для побитовых операций. reset
определяет границы продолжения и shift
берет продолжение из стека вызовов.
Примечания)
(*) В Scala продолжение заканчивается там, где reset
заканчивается блок. Другой возможный подход - позволить ей заканчиваться там, где заканчивается функция.
(**) Одним из параметров вызываемого кода является адрес возврата, показывающий, какая часть вызывающего кода еще не была выполнена. Что ж, в Scala для этого используется последовательность адресов возврата. Как много? Все адреса возврата помещаются в стек вызовов с момента входа в reset
блок.
UPD Part 2
Отбрасывание продолжений: фильтрация
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
Это печатает:
0 2 4 6 8 10
Выделим две важные операции: отказ от продолжения ( fail()
) и передача ему управления ( succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
Обе версии 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()
:
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 ")
}
Выше, если x
четное, выдается исключение и продолжение не вызывается; если x
нечетное, исключение не вызывается и вызывается продолжение. Приведенный выше код печатает:
1 3 5 7 9