Продолжение 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. Зачем? Мы вызываем from0to10as 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