Продовження масштабу на значущих прикладах
Визначимо, 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як 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блок.
Такий метод повернення контролю назад називається зворотним відстеженням , це дуже стара техніка, відома принаймні з часів похідних Пролога та ШІ, орієнтованих на ШІ.
Імена resetта shiftнеправильні назви . Ці імена краще було б залишити для побітових операцій. resetвизначає межі продовження та shiftбере продовження зі стеку викликів.
Примітка
(*) У Scala продовження закінчується там, де resetзакінчується блок. Іншим можливим підходом було б дати йому закінчитися там, де закінчується функція.
(**) Одним із параметрів викликаного коду є адреса повернення, яка показує, яка частина викличного коду ще не виконана. Ну, у Scala для цього використовується послідовність адрес повернення. Скільки? Усі адреси повернення, розміщені в стеці викликів з моменту введення resetблоку.
UPD Частина 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