Я просматривал всю сеть в поисках информации о продолжениях, и мне ошеломляет, как простейшие объяснения могут так сильно сбить с толку такого программиста на JavaScript, как я. Это особенно верно, когда в большинстве статей объясняются продолжения с помощью кода на Scheme или используются монады.
Теперь, когда я, наконец, думаю, что понял суть продолжений, я хотел знать, правда ли то, что я знаю. Если то, что я считаю правдой, на самом деле неправда, то это невежество, а не просветление.
Итак, вот что я знаю:
Почти во всех языках функции явно возвращают значения (и управление) вызывающей стороне. Например:
var sum = add(2, 3);
console.log(sum);
function add(x, y) {
return x + y;
}
Теперь на языке с функциями первого класса мы можем передать элемент управления и возвращаемое значение функции обратного вызова вместо явного возврата вызывающей стороне:
add(2, 3, function (sum) {
console.log(sum);
});
function add(x, y, cont) {
cont(x + y);
}
Таким образом, вместо того, чтобы возвращать значение из функции, мы продолжаем с другой функцией. Поэтому эта функция называется продолжением первой.
Так в чем же разница между продолжением и обратным вызовом?
источник
Ответы:
Я считаю, что продолжения - это особый случай обратных вызовов. Функция может вызывать любое количество функций, любое количество раз. Например:
Однако, если функция вызывает в качестве последней функции другую функцию, тогда вторая функция называется продолжением первой. Например:
Если функция вызывает другую функцию как последнее, что она делает, она называется хвостовым вызовом. Некоторые языки, такие как Scheme, выполняют оптимизацию хвостовых вызовов. Это означает, что хвостовой вызов не влечет за собой полную нагрузку при вызове функции. Вместо этого он реализован в виде простого перехода (с заменой кадра стека вызывающей функции на кадр стека хвостового вызова).
Бонус : переход к продолжению прохождения стиля. Рассмотрим следующую программу:
Теперь, если бы каждая операция (включая сложение, умножение и т. Д.) Была записана в виде функций, то мы бы получили:
Кроме того, если бы нам не разрешали возвращать какие-либо значения, нам пришлось бы использовать продолжения следующим образом:
Этот стиль программирования, в котором вам не разрешено возвращать значения (и, следовательно, вы должны прибегать к передаче продолжения), называется стилем передачи продолжения.
Однако есть две проблемы со стилем прохождения продолжения:
Первая проблема может быть легко решена в JavaScript путем асинхронного вызова продолжений. Вызывая продолжение асинхронно, функция возвращается до вызова продолжения. Следовательно, размер стека вызовов не увеличивается:
Вторая проблема обычно решается с помощью вызываемой функции,
call-with-current-continuation
которая часто сокращается доcallcc
. К сожалению,callcc
не может быть полностью реализовано в JavaScript, но мы могли бы написать функцию замены для большинства случаев использования:callcc
Функция принимает функциюf
и применяет его кcurrent-continuation
(сокращенноcc
). Функцияcurrent-continuation
продолжения, которая оборачивает остальную часть тела функции после вызоваcallcc
.Рассмотрим тело функции
pythagoras
:current-continuation
Второйcallcc
является:Точно так же
current-continuation
из первыхcallcc
:Поскольку
current-continuation
первыйcallcc
содержит другой,callcc
он должен быть преобразован в стиль передачи продолжения:Таким образом, по сути
callcc
логически преобразует все тело функции обратно в то, с чего мы начали (и дает этим анонимным функциям имяcc
). Функция pythagoras, использующая эту реализацию callcc, становится тогда:Опять же, вы не можете реализовать
callcc
в JavaScript, но вы можете реализовать его в стиле передачи продолжения в JavaScript следующим образом:Эта функция
callcc
может использоваться для реализации сложных структур управления потоками, таких как блоки try-catch, сопрограммы, генераторы, волокна и т. Д.источник
Несмотря на замечательную рецензию, я думаю, что вы немного путаете свою терминологию. Например, вы правы в том, что хвостовой вызов происходит, когда вызов - это последнее, что должна выполнить функция, но в отношении продолжений хвостовой вызов означает, что функция не изменяет продолжение, с которым она вызывается, только то, что обновляет значение, переданное продолжению (если оно того пожелает). Вот почему конвертировать хвостовую рекурсивную функцию в CPS так просто (вы просто добавляете продолжение в качестве параметра и вызываете продолжение в результате).
Также немного странно называть продолжения частным случаем обратных вызовов. Я вижу, как их легко сгруппировать, но продолжения не возникли из-за необходимости отличать их от обратного вызова. Продолжение фактически представляет инструкции, остающиеся для завершения вычисления , или оставшуюся часть вычисления с этого момента времени. Вы можете думать о продолжении как о дыре, которую нужно заполнить. Если я смогу зафиксировать текущее продолжение программы, тогда я смогу вернуться к тому, какой была программа, когда я захватил продолжение. (Это, безусловно, облегчает написание отладчиков.)
В этом контексте ответ на ваш вопрос заключается в том, что обратный вызов - это общая вещь, которая вызывается в любой момент времени, указанный в каком-то контракте, предоставленном вызывающей стороной [обратного вызова]. Обратный вызов может иметь столько аргументов, сколько он хочет, и быть структурированным так, как он хочет. Продолжение , то обязательно процедура один аргумент , который решает значение , переданное в него. Продолжение должно быть применено к одному значению, и приложение должно произойти в конце. Когда продолжение заканчивается, выполнение выражения завершается, и, в зависимости от семантики языка, побочные эффекты могут возникать или не возникать.
источник
Краткий ответ заключается в том, что различие между продолжением и обратным вызовом состоит в том, что после того, как обратный вызов вызван (и завершился), выполнение возобновляется с той точки, в которой оно было вызвано, а при вызове продолжения выполнение возобновляется с того момента, когда было создано продолжение. Другими словами: продолжение никогда не возвращается .
Рассмотрим функцию:
(Я использую синтаксис Javascript, хотя Javascript на самом деле не поддерживает первоклассные продолжения, потому что это было то, что вы привели в качестве примеров, и это будет более понятным для людей, не знакомых с синтаксисом Lisp.)
Теперь, если мы передадим ему обратный вызов:
тогда мы увидим три предупреждения: «до», «5» и «после».
С другой стороны, если мы передадим ему продолжение, которое делает то же самое, что и обратный вызов, например:
тогда мы увидим только два предупреждения: «до» и «5». Вызов
c()
insideadd()
завершает выполнениеadd()
и вызываетcallcc()
возврат; возвращаемое значениеcallcc()
было переданным в качестве аргументаc
(а именно, суммой).В этом смысле, даже если вызов продолжения выглядит как вызов функции, в некотором смысле он больше похож на оператор возврата или выброс исключения.
Фактически, call / cc можно использовать для добавления операторов возврата к языкам, которые их не поддерживают. Например, если в JavaScript не было оператора return (вместо этого, как и во многих языках Lips, просто возвращалось значение последнего выражения в теле функции), но был call / cc, мы могли бы реализовать return следующим образом:
Вызов
return(i)
вызывает продолжение, которое завершает выполнение анонимной функции и приводитcallcc()
к возврату индекса,i
по которомуtarget
был найден вmyArray
.(NB: есть некоторые способы, в которых аналогия с «возвратом» является немного упрощенной. Например, если продолжение ускользает от функции, в которой оно было создано - например, путем сохранения где-то в глобальном масштабе - возможно, что функция , который создал продолжение, может возвращаться несколько раз, даже если оно было вызвано только один раз .)
Call / cc может аналогичным образом использоваться для реализации обработки исключений (throw и try / catch), циклов и многих других структур управления.
Чтобы прояснить некоторые возможные заблуждения:
Оптимизация хвостового вызова никоим образом не требуется для поддержки первоклассных продолжений. Учтите, что даже язык C имеет (ограниченную) форму продолжений в форме
setjmp()
, которая создает продолжение, иlongjmp()
, которая вызывает его!Нет особой причины, по которой продолжение должно принимать только один аргумент. Просто аргумент (ы) продолжения становится возвращаемым значением (ями) call / cc, а call / cc обычно определяется как имеющий единственное возвращаемое значение, поэтому, естественно, продолжение должно принимать ровно одно. В языках с поддержкой нескольких возвращаемых значений (таких как Common Lisp, Go или даже Scheme) вполне возможно иметь продолжения, которые принимают несколько значений.
источник