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


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

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

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

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

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

6 83

6 ответов:

мой блог объясняет, что reset и shift сделать, так что вы можете прочитать это снова.

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

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

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

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

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

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

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

учитывая канонический пример из исследование с разделителями для продолжения Скала, немного модифицированная, так что входной функции 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 " to " я зову тебя внутрь 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 не имеет вывода глобального типа Хаскелла и, следовательно, насколько я знаю, не может поддерживать неявное поднятие до состояния монады unit (как одна из возможных стратегий для скрытия явной среды), потому что глобальный вывод типа Хаскелла (Хиндли-Милнера) зависит от не поддерживает алмазное множественное виртуальное наследование.

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

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

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

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

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

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

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

еще одна (более поздняя-май 2016) статья о продолжениях Scala:
"путешествие во времени в Scala: CPS в Scala (продолжение scala) " by Shivansh Шривастава (shiv4nsh).
Это также относится к Джим McBeath ' s статьи упомянутые в Дмитрий Беспалов ' s ответ.

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

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

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

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

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

это было сказано, как объявлено в апрель 2014 для Scala 2.11.0-RC1

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