Что такое call / cc?

Я несколько раз пытался понять концепцию продолжения и вызов/cc. Каждая попытка была неудачной. Может кто-нибудь объяснить мне эти концепции, в идеале с более реалистичными примерами, чем в Википедии или в других сообщениях SO.

У меня есть опыт в веб-программировании и ООП. Я также понимаю сборку 6502 и имел незначительный рандеву с Эрлангом. Тем не менее, я не могу обернуть голову вокруг call/cc.

10 ответов


Смотри, я нашел это Продолжение Прохождения Стиле лучшее описание по этой теме.

вот лишенная подробностей копия этой статьи:

Автор: Marijn Haverbeke Дата: 24 июля 2007 года

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

function traverseDocument(node, func) {
  func(node);
  var children = node.childNodes;
  for (var i = 0; i < children.length; i++)
    traverseDocument(children[i], func);
}   

function capitaliseText(node) {
  if (node.nodeType == 3) // A text node
    node.nodeValue = node.nodeValue.toUpperCase();
}

traverseDocument(document.body, capitaliseText);

Это можно преобразовать следующим образом: мы добавляем дополнительный аргумент к каждой функции, который будет использоваться для передачи продолжения функции. Это продолжение является значением функции, представляющим действия, которые должны произойти после "возвращения" функции. Стек (вызов) устаревает в стиле продолжения передачи-когда функция вызывает другую функцию, это последнее, что она делает. Вместо того, чтобы ждать вызванная функция для возврата, она помещает любую работу, которую она хочет сделать, в продолжение, которое она передает функции.

function traverseDocument(node, func, c) {
  var children = node.childNodes;
  function handleChildren(i, c) {
    if (i < children.length)
      traverseDocument(children[i], func,
                       function(){handleChildren(i + 1, c);});
    else
      c();
  }
  return func(node, function(){handleChildren(0, c);});
}

function capitaliseText(node, c) {
  if (node.nodeType == 3)
    node.nodeValue = node.nodeValue.toUpperCase();
  c();
}

traverseDocument(document.body, capitaliseText, function(){});

представьте, что у нас есть документ huuuuuge для капитализации. Просто прохождение его за один раз занимает пять секунд, а замораживание браузера на пять секунд-довольно плохой стиль. Рассмотрим эту простую модификацию capitaliseText (не обращайте внимания на уродливый глобальный):

var nodeCounter = 0;
function capitaliseText(node, c) {
  if (node.nodeType == 3)
    node.nodeValue = node.nodeValue.toUpperCase();

  nodeCounter++;
  if (nodeCounter % 20 == 0)
    setTimeout(c, 100);
  else
    c();
}

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

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


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

(define x 0) ; dummy value - will be used to store continuation later

(+ 2 (call/cc (lambda (cc)
                (set! x cc)  ; set x to the continuation cc; namely, (+ 2 _)
                3)))         ; returns 5

(x 4) ; returns 6

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

(* 123 (+ 345 (* 789 (x 5)))) ; returns 7

  reason: it is because (x 5) replaces the existing continuation,
          (* 123 (+ 345 (* 789 _))), with x, (+ 2 _), and returns
          5 to x, creating (+ 2 5), or 7.

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


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

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

другим способом использования продолжения было бы думаю о замене вызовы метода с несколькими нитевидными структурами которые сосуществуют параллельно (либо запущенные, либо приостановленные), передавая управление друг другу, используя контексты продолжения вместо "классического"call парадигмы. Они будут работать на глобальных (общих) данных, а не полагаться на параметры. Это в какой-то степени более гибко, чем call в том смысле, что стек не должен заводиться, а затем вниз (calls are вложенные), но контроль может проходить произвольно.

попытка визуализировать эту концепцию на языке такой C, представьте, что один большой цикл с одним switch(continuation_point) { case point1: ... } заявление, где каждая case соответствует продолжению-savepoint, и где код внутри каждого case изменить значение continuation_point и отказаться от контроля над этим continuation_point by breaking от switch и привлечение к следующей итерации в цикле.

каков контекст вашего вопроса? Какие-то конкретные сценарии вас интересуют? Любой конкретный язык программирования? Достаточно ли примера thread/fibre выше?


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

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

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

редактировать на основе комментария:

продолжение полного исполнения. В любой момент выполнения вы можете разделить программу на две части (во времени, а не в пространстве) - ту, которая этот пункт и все, что будет дальше. "Текущее продолжение " - это" все, что будет работать отсюда " (вы можете думать об этом как о функции, которая будет делать все, что сделала бы остальная часть вашей программы). Таким образом, функция, которую вы предоставляете call/cc получает переданное продолжение, которое было текущим, когда . Функция может использовать продолжение для возврата выполнения в call/cc заявление (более вероятно, хотя он передаст продолжение вокруг к чему-то еще, потому что если бы он использовал его напрямую, он мог бы сделать простой возврат).


когда я пытался понять call/cc, я нашел это call-with-current-continuation-for-C-programmers страница была полезной.


представьте, что ваш сценарий-это видео-игра стадии. Call/cc - это как бонусный этап.

parellel between bonus stage and call/cc

как только вы касаетесь его, вы переноситесь на бонусный этап (т. е. определение функции, переданной в качестве аргумента для вызова/cc [f в этом случае]).

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

parellel between exit bonus stage and call/cc function args

так это не имеет значения, если есть много args, когда вы достигнете одного из них, это закончится. Так наша казнь достигает (arg 42) и возвращает его в сумме (+ 42 10).

также есть некоторые замечания стоит отметить:

  • не все функции можно использовать с call / cc. Поскольку его ожидает продолжение (это функция), вы не можете иметь f как этот: (define f (lambda (k) (+ k 42)) , потому что вы не можете sum a функция.
  • также вы не можете иметь (define f (lambda (k) (f 42 10))) потому что продолжение ожидает только один аргумент.
  • вы можете закончить без touching любой выход, в этом случае функция продолжается как любая обычная функция (например,(define f (lambda (k) 42) отделки и возвращает 42).

лучшее объяснение, которое я видел в книге Пола Грэхема,На Lisp.


существует несколько уровней для понимания вызова / cc. Сначала вам нужно понять термины и то, как работает механизм. Затем понимание того, как и когда call/cc используется в " реальной жизни" необходимо Программирование.

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

для второго уровня я рекомендую следующую классику Фридмана.

Даниэль П. Фридман. "Applications of Continuations: Invited Tutorial". Одна тысяча девятьсот восемьдесят восемь Принципы языков программирования (POPL88). Января 1988 года.


модель, которую я использовал для понимания продолжения с императивной точки зрения, заключается в том, что это копия стека вызовов в сочетании с указателем a на следующую инструкцию.

Call / cc вызывает функцию (переданную как аргумент) с продолжением в качестве аргумента.


взгляните на описание и реализацию call / cc для FScheme: http://blogs.msdn.com/b/ashleyf/archive/2010/02/11/turning-your-brain-inside-out-with-continuations.aspx