Что такое продолжения Scala и зачем их использовать?

Я только что закончил Программирование на Scala, и я изучал изменения между Scala 2.7 и 2.8. Самым важным кажется плагин продолжения, но я не понимаю, для чего он полезен и как работает. Я видел, что это хорошо для асинхронного ввода-вывода, но не смог понять, почему. Вот некоторые из наиболее популярных ресурсов по этой теме:

И этот вопрос о переполнении стека:

К сожалению, ни одна из этих ссылок не пытается определить, для чего нужны продолжения или что должны делать функции сдвига / сброса, и я не нашел никаких ссылок, которые бы это делали. Я не мог догадаться, как работает какой-либо из примеров в связанных статьях (или что они делают), поэтому один из способов помочь мне - это построчно просмотреть один из этих примеров. Даже этот простой из третьей статьи:

reset {
    ...
    shift { k: (Int=>Int) =>  // The continuation k will be the '_ + 1' below.
        k(7)
    } + 1
}
// Result: 8

Почему результат 8? Это, вероятно, поможет мне начать работу.


person Dave    schedule 03.10.2009    source источник
comment
scala-lang.org/api/current/   -  person Vadzim    schedule 04.12.2013


Ответы (7)


В моем блоге действительно объясняется, что делают reset и shift, поэтому возможно, вы захотите прочитать это еще раз.

Еще один хороший источник, на который я также указываю в своем блоге, - это запись в Википедии о стиле прохождения продолжения. Этот вариант, безусловно, является наиболее ясным по предмету, хотя он не использует синтаксис Scala, а продолжение передается явно.

В статье о разграниченных продолжениях, на которую я ссылаюсь в своем блоге, но которая, кажется, не работает, приводится много примеров использования.

Но я думаю, что лучшим примером концепции разделенных продолжений является Scala Swarm. В нем библиотека останавливает выполнение вашего кода в одной точке, а оставшееся вычисление становится продолжением. Затем библиотека что-то делает - в данном случае передает вычисление на другой хост и возвращает результат (значение переменной, к которой был осуществлен доступ) вычислению, которое было остановлено.

Теперь вы не понимаете даже простого примера на странице Scala, поэтому обязательно прочтите мой блог. В нем я только заинтересован в объяснении этих основ того, почему результат 8.

person Daniel C. Sobral    schedule 03.10.2009
comment
Я перечитал вашу запись в блоге и на этот раз остановился на ней - думаю, я лучше понимаю, что происходит. Я не получил многого от страницы Википедии (я уже знаю продолжения Lisp), но стиль отложенного сброса / сдвига или что-то еще, что он вызвал, поставил меня в тупик. Для нетерпеливых (то есть меня) ваше описание было в порядке, но люди должны будут придерживаться его до пункта. Результат сброса является результатом кода внутри сдвига. абзац ... Я безнадежно заблудился до этого момента, но становится яснее! Я посмотрю на Swarm, потому что мне все еще любопытно, для чего он нужен. Спасибо! - person Dave; 04.10.2009
comment
Да, нужно время, чтобы все обретало смысл. Я не чувствовал, что смогу быстрее уйти от объяснения. - person Daniel C. Sobral; 04.10.2009
comment
Все это сошлось для меня, когда я пришел к осознанию того, что перезагрузка ограничивает возможности продолжения. (то есть: включаемые переменные и операторы.) - person JeffV; 04.10.2009
comment
Ваше объяснение было многословным и не доходило до сути понимания. Примеры были длинными, я не получил достаточно понимания в первых абзацах, чтобы вдохновить меня прочитать все это. Так что я проголосовал против. SO отображает сообщение после того, как я проголосую, с просьбой добавить комментарий, поэтому я подчиняюсь. Прошу прощения за мою откровенность. - person Shelby Moore III; 05.11.2012
comment
@ShelbyMooreIII Не нужно извиняться. Я ценю критику. - person Daniel C. Sobral; 05.11.2012
comment
Спасибо за Ваше понимание. Несколько минут назад я добавил свой ответ, и если хотите, вы можете оставить свой отзыв. - person Shelby Moore III; 05.11.2012
comment
Я писал об этом в блоге, уделяя особое внимание пониманию потока управления (без обсуждения деталей реализации). wherenullpoints.com/2014/04/scala-continuations.html - person Alexandros; 09.04.2014
comment
Ого, это взорвало мне голову. Раньше я представлял, как калькуляторы вычитают из оператора. Это вроде как обобщение, если я понимаю. - person acjay; 17.04.2014

Я обнаружил, что существующие объяснения менее эффективны для объяснения концепции, чем я бы надеялся. Надеюсь, это ясно (и правильно). Я еще не использовал продолжения.

Когда вызывается функция продолжения cf:

  1. Execution skips over the rest of the shift block and begins again at the end of it
    • the parameter passed to cf is what the shift block "evaluates" to as execution continues. this can be different for every call to cf
  2. Execution continues until the end of the reset block (or until a call to reset if there is no block)
    • the result of the reset block (or the parameter to reset() if there is no block) is what cf returns
  3. Выполнение продолжается с cf до конца shift блока.
  4. Выполнение пропускается до конца блока reset (или до вызова сброса?)

Итак, в этом примере следуйте буквам от A до Z

reset {
  // A
  shift { cf: (Int=>Int) =>
    // B
    val eleven = cf(10)
    // E
    println(eleven)
    val oneHundredOne = cf(100)
    // H
    println(oneHundredOne)
    oneHundredOne
  }
  // C execution continues here with the 10 as the context
  // F execution continues here with 100
  + 1
  // D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven
  // G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne
}
// I

Это печатает:

11
101
person Alex Neth    schedule 05.11.2009
comment
У меня есть ошибка, в которой говорится, что невозможно вычислить тип для результата функции, преобразованной в CPS, когда я пытался ее скомпилировать .. я не уверен, что это такое, и как это исправить - person Fabio Veronez; 29.03.2011
comment
@Fabio Veronez Добавьте оператор return в конец смены: измените println(oneHundredOne) } на, скажем, println(oneHundredOne); oneHundredOne }. - person folone; 23.06.2011
comment
Хорошее объяснение ужасного синтаксиса. Объявление функции продолжения странным образом отделено от своего тела. Я бы не хотел делиться таким ломающим голову кодом с другими. - person joeytwiddle; 10.06.2012
comment
Чтобы избежать ошибки cannot compute type for CPS-transformed function result, +1 должен следовать сразу после oneHundredOne}. Комментарии, которые в данный момент находятся между ними, каким-то образом нарушают грамматику. - person lcn; 13.01.2015

Учитывая канонический пример из исследовательской статьи для ограниченных продолжений Scala , немного изменен, поэтому функции, входящей в shift, присвоено имя f и, таким образом, больше не является анонимным.

def f(k: Int => Int): Int = k(k(k(7)))
reset(
  shift(f) + 1   // replace from here down with `f(k)` and move to `k`
) * 2

Подключаемый модуль Scala преобразует этот пример таким образом, что вычисление (во входном аргументе reset), начиная с каждого shift до вызова reset, заменяется входом функции (например, f) в shift.

Замененное вычисление сдвигается (т. Е. Перемещается) в функцию k. Функция f вводит функцию k, где k содержит замененное вычисление, k вводит x: Int, а вычисление в k заменяет shift(f) на x.

f(k) * 2
def k(x: Int): Int = x + 1

Что имеет тот же эффект, что и:

k(k(k(7))) * 2
def k(x: Int): Int = x + 1

Обратите внимание, что тип Int входного параметра x (т.е. сигнатура типа k) был задан сигнатурой типа входного параметра f.

Другой заимствованный пример с концептуально эквивалентной абстракцией, т. Е. read - это ввод функции в shift:

def read(callback: Byte => Unit): Unit = myCallback = callback
reset {
  val byte = "byte"

  val byte1 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "1 = " + byte1)
  val byte2 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "2 = " + byte2)
}

Я считаю, что это можно было бы перевести в логический эквивалент:

val byte = "byte"

read(callback)
def callback(x: Byte): Unit {
  val byte1 = x
  println(byte + "1 = " + byte1)
  read(callback2)
  def callback2(x: Byte): Unit {
    val byte2 = x
    println(byte + "2 = " + byte1)
  }
}

Я надеюсь, что это проясняет целостную общую абстракцию, которая была несколько запутана предыдущим представлением этих двух примеров. Например, канонический первый пример был представлен в исследовательском документе в качестве анонимной функции вместо моего имени f, поэтому некоторым читателям не сразу стало ясно, что она абстрактно аналогична read в заимствовал второй пример.

Таким образом, ограниченные продолжения создают иллюзию инверсии контроля от «вы зовете меня извне reset» до «я зову вас внутри reset».

Обратите внимание, что тип возвращаемого значения f есть, а k - нет, он должен совпадать с типом возвращаемого значения reset, т.е. f имеет право объявлять любой тип возврата для k, пока f возвращает тот же тип, что и reset. То же самое для read и capture (см. Также ENV ниже).


Разграниченные продолжения неявно инвертируют управление состоянием, например read и callback не являются чистыми функциями. Таким образом, вызывающий не может создавать ссылочно прозрачные выражения и, следовательно, не имеет декларативного (также известного как прозрачный) контроль над предполагаемым императивным семантика.

Мы можем явно достичь чистых функций с ограниченными продолжениями.

def aread(env: ENV): Tuple2[Byte,ENV] {
  def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback)
  shift(read)
}
def pure(val env: ENV): ENV {
  reset {
    val (byte1, env) = aread(env)
    val env = env.println("byte1 = " + byte1)
    val (byte2, env) = aread(env)
    val env = env.println("byte2 = " + byte2)
  }
}

Я считаю, что это можно было бы перевести в логический эквивалент:

def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV =
  env.myCallback(callback)
def pure(val env: ENV): ENV {
  read(callback,env)
  def callback(x: Tuple2[Byte,ENV]): ENV {
    val (byte1, env) = x
    val env = env.println("byte1 = " + byte1)
    read(callback2,env)
    def callback2(x: Tuple2[Byte,ENV]): ENV {
      val (byte2, env) = x
      val env = env.println("byte2 = " + byte2)
    }
  }
}

Это становится шумно из-за явного окружения.

Кстати, в Scala нет вывода глобального типа Haskell, и поэтому, насколько мне известно, он не может поддерживать неявный подъем до состояния unit монады состояния (как одна из возможных стратегий для сокрытия явного окружения), потому что глобальный тип Haskell (Hindley-Milner) вывод зависит от <алмазного множественного виртуального наследования >.

person Shelby Moore III    schedule 05.11.2012
comment
Я предлагаю заменить _1 _ / _ 2_ на _3 _ / _ 4_. По соглашению, f и read будут with, а k и callback будут replaced, captured, continuation или callback. - person Shelby Moore III; 06.11.2012
comment
with - ключевое слово. P.S. Некоторые из ваших сбросов имеют (), что должно быть {} В любом случае отличное описание! - person nafg; 06.11.2012
comment
@nafg спасибо, поэтому я предложу replacement вместо with. Афаик, () тоже можно? Afaik, {} - это облегченный синтаксис Scala для замыканий, который скрытие вызова базовой функции. Например, посмотрите, как я переписал sequence Дэниела (обратите внимание, что код никогда не компилировался или проверено, поэтому, пожалуйста, поправьте меня). - person Shelby Moore III; 06.11.2012
comment
Блок, то есть выражение, содержащее несколько операторов, требует фигурных скобок. - person nafg; 06.11.2012
comment
@nafg, правильно. Afaik shift reset - это библиотечные функции, а не ключевые слова. Таким образом, {} или () можно использовать, когда функция ожидает только один параметр. Scala имеет параметры By-name (см. Раздел 9.5 «Абстракции управления программированием в Scala», 2-е изд. Стр. 218), где, если параметр имеет тип () => ..., () => можно исключить. Я предполагаю Unit, а не по имени, потому что блок должен оцениваться до вызова reset, но мне нужно {} для нескольких операторов. Я правильно использую shift, потому что он явно вводит тип функции. - person Shelby Moore III; 06.11.2012
comment
() = ›Исключить нельзя. Почему бы тебе не запустить REPL? - person nafg; 07.11.2012
comment
@nafg на странице 219 книги «Программирование на Scala», 2-е изд., есть пример с параметром по имени, и он показывает, что () => исключен из входного аргумента. Конечно, функция обратного вызова без входных параметров по-прежнему создается и вызывается за кулисами, но она исключена из синтаксиса аргументов. Хе-хе, вы пришли к выводу, что я часто кодирую Scala только в голове. Я делаю это, потому что я думаю о более крупной картинке в концептуальном + редукционистском отношении к языковому дизайну на данный момент, и не хочу запутаться + погрязнуть во взрыве заумных, бессмысленных, мелких мелочей, диареи. :) Спасибо. - person Shelby Moore III; 07.11.2012
comment
У меня нет книги, вы можете назвать конкретный код? Может я просто неправильно тебя понял. Может быть исключено, подразумевает наличие необязательного синтаксиса, наличие двух взаимозаменяемых синтаксисов. Я подозреваю, что на самом деле вы говорите о параметрах по имени, которые являются синтаксическим сахаром, заставляющим за кулисами обрабатывать x как () = ›x. () = ›X также будет интерпретироваться как () =› () = ›x. Опять же, это за кадром. Синтаксис определения, конечно же, = ›, без (). - person nafg; 12.11.2012
comment
@nafg да я написал перед параметрами By-name. Вместо того, чтобы писать reset( () => { ... } ), Сахар по имени плюс опция Scala для записи аргументов после имени функции, разделенных пробелом, позволяет мне писать reset { ... } на сайте вызова функции (т. Е. Использования). Да, для объявления параметра By-name требуется => на сайте определения функции, например def reset( param : => Unit ). Также я предполагаю, что reset( ... ) также является допустимым вызовом функции, если ... оценивается как Unit. Терминология, аргументы для параметров предоставляются на месте вызова. - person Shelby Moore III; 18.02.2013

Продолжение фиксирует состояние вычисления, которое будет вызвано позже.

Подумайте о вычислении между выходом из выражения сдвига и выходом из выражения сброса как функции. Внутри выражения сдвига эта функция называется k, это продолжение. Вы можете передавать его, вызывать позже, даже более одного раза.

Я думаю, что значение, возвращаемое выражением сброса, является значением выражения внутри выражения сдвига после =>, но в этом я не совсем уверен.

Таким образом, с продолжениями вы можете заключить в функцию довольно произвольный и нелокальный фрагмент кода. Это можно использовать для реализации нестандартного потока управления, такого как сопрограммы или обратное отслеживание.

Таким образом, продолжения следует использовать на системном уровне. Внедрение их через код вашего приложения было бы верным рецептом для кошмаров, гораздо хуже, чем самый худший спагетти-код с использованием goto, который когда-либо мог быть.

Отказ от ответственности: у меня нет глубокого понимания продолжений в Scala, я просто сделал вывод, глядя на примеры и зная продолжения из Scheme.

person starblue    schedule 03.10.2009

С моей точки зрения, лучшее объяснение было дано здесь: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

Один из примеров:

Чтобы более четко увидеть поток управления, вы можете выполнить этот фрагмент кода:

reset {
    println("A")
    shift { k1: (Unit=>Unit) =>
        println("B")
        k1()
        println("C")
    }
    println("D")
    shift { k2: (Unit=>Unit) =>
        println("E")
        k2()
        println("F")
    }
    println("G")
}

Вот результат, который дает приведенный выше код:

A
B
D
E
G
F
C
person Dmitry Bespalov    schedule 19.04.2016

Еще одна (более свежая - май 2016 г.) статья о продолжениях Scala:
"Путешествие во времени в Scala: CPS в Scala (продолжение scala)" от Шиванш Шривастава (shiv4nsh).
Это также относится к Джиму Макбиту статья, упомянутая в ответ Дмитрия Беспалова.

Но перед этим он описывает продолжения так:

Продолжение - это абстрактное представление состояния управления компьютерной программы.
На самом деле это означает, что это структура данных, которая представляет вычислительный процесс в заданный момент его выполнения. ; Созданная структура данных может быть доступна для языка программирования, вместо того, чтобы быть скрытой в среде выполнения.

Чтобы объяснить это дальше, у нас есть один из самых классических примеров,

Допустим, вы находитесь на кухне перед холодильником и думаете о сэндвиче. Вы берете продолжение прямо здесь и кладете его в карман.
Затем вы достаете индейку и хлеб из холодильника и делаете себе бутерброд, который теперь лежит на прилавке.
Вы вызываете продолжение в своем карман, и вы снова стоите перед холодильником, думая о бутерброде. Но, к счастью, на прилавке есть бутерброд, а все материалы, из которых он был сделан, исчезли. Итак, вы его съедите. :-)

В этом описании sandwich является частью данных программы (например, объекта в куче), и вместо того, чтобы вызывать подпрограмму «make sandwich» и затем возвращаться, человек вызвал подпрограмму «make sandwich with current continuation» , который создает бутерброд, а затем продолжается с того места, где было остановлено выполнение.

При этом, как объявлено в Апрель 2014 г. для Scala 2.11.0-RC1

Мы ищем специалистов по сопровождению следующих модулей: scala-swing, scala-continueations.
2.12 не будет включать их, если не будет найден новый сопровождающий .
Мы, вероятно, продолжим поддерживать другие модули (scala-xml, scala-parser-combinators), но помощь по-прежнему приветствуется.

person VonC    schedule 01.08.2016

Продолжение 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