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

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

И этот вопрос о Stack Overflow:

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

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

Почему результат 8? Возможно, это поможет мне начать.

Ответы

Ответ 1

My blog объясняет, что делают reset и shift, поэтому вы можете снова прочитать это.

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

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

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

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

Ответ 2

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

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

  • Выполнение пропускает оставшуюся часть блока shift и начинается снова в конце
    • параметр, переданный в cf, является тем, что блок shift "оценивает", когда выполнение продолжается. это может быть различным для каждого вызова cf
  • Выполнение продолжается до конца блока reset (или до вызова reset, если нет блока)
    • результат блока reset (или параметр reset(), если нет блока) - это то, что cf возвращает
  • Выполнение продолжается после cf до конца блока shift
  • Выполнение пропускается до конца блока reset (или вызов 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

Ответ 3

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

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

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

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

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

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

Ответ 4

Учитывая канонический пример из исследовательской статьи для Scala ограниченных продолжений, слегка измененных, поэтому функция, вводимая в shift, дается name 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 не являются чистыми функциями. Таким образом, вызывающий не может создавать ссылочно прозрачные выражения и, следовательно, не имеет декларативный (a.k.a. прозрачный) контроль над предполагаемой императивной семантикой.

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

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) тип вывода зависит от не поддерживает множественное виртуальное наследование алмазов.

Ответ 5

С моей точки зрения, лучшее объяснение было дано здесь: 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

Ответ 6

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

Но до этого он описывает Continuations следующим образом:

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

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

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

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

Как сказано в Апрель 2014 для Scala 2.11.0-RC1

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