Продовження масштабу на значущих прикладах
Визначимо, 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