реализация call / cc? - PullRequest
       50

реализация call / cc?

26 голосов
/ 29 января 2012

Я пытаюсь найти, как реализован call / cc.Лучшее, что я нашел, это фрагмент кода на Haskell:

callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k

Хотя это не так просто, как хотелось бы, из-за Cont и runCont.Я также нашел описания того, что он делает, хотя и не так ясно, как реальный код.

Так как это реализовано в простейшей форме?Я отмечаю это с помощью Scheme и Haskell, так как это два языка, которые я предпочитаю.

Ответы [ 5 ]

81 голосов
/ 29 января 2012

«Реализация call/cc» на самом деле не имеет смысла для слоя, на котором вы работаете;если вы можете реализовать call/cc на языке, это означает, что он имеет встроенную конструкцию, по крайней мере такую ​​же мощную, как call/cc.На уровне самого языка call/cc по сути является примитивным оператором потока управления, точно так же, как должна быть некоторая форма ветвления.

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

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

Технически,это определение callCC действительно наберет, если вы просто удалите приложения Cont и runCont.Но это не поможет вам понять, как это работает в контексте монады Cont, поэтому давайте посмотрим на ее определение.(Это определение не используется в текущей библиотеке Monad Transformer Library, поскольку все монады в ней построены поверх своих версий преобразователя, но оно соответствует использованию фрагмента Cont (который работает только с более старой версией).) и значительно упрощает.)

newtype Cont r a = Cont { runCont :: (a -> r) -> r }

ОК, поэтому Cont r a - это просто (a -> r) -> r, а runCont позволяет нам получить эту функцию из значения Cont r a.Достаточно просто.Но что это значит?

Cont r a - это вычисление с прохождением продолжения с конечным результатом r и результатом a.Что означает окончательный результат?Хорошо, давайте напишем тип runCont более явно:

runCont :: Cont r a -> (a -> r) -> r

Итак, как мы видим, «конечный результат» - это значение, которое мы получаем из runCont в конце.Теперь, как мы можем построить вычисления с Cont?Экземпляр монады просветляет:

instance Monad (Cont r) where
  return a = Cont (\k -> k a)
  m >>= f = Cont (\k -> runCont m (\result -> runCont (f result) k))

Хорошо, хорошо, это просветление, если вы уже знаете, что это значит.Ключевым моментом является то, что когда вы пишете Cont (\k -> ...), k равняется остальной части вычисления - он ожидает, что вы дадите ему значение a, а затем даст вам окончательный результатвычисление (типа r, запомните) назад, которое вы затем можете использовать в качестве собственного возвращаемого значения, потому что ваш тип возврата тоже r.Уф!И когда мы запускаем вычисление Cont с runCont, мы просто указываем окончательный k - «верхний уровень» вычисления, который дает конечный результат.

Что это за остальная частьвычисление "называется? продолжение, , потому что это продолжение вычисления!

(>>=) на самом деле довольно просто: мы запускаем вычисление слева, давая ему собственный остаток вычислений.Этот остаток вычислений просто передает значение в f, который производит свои собственные вычисления.Мы выполняем это вычисление, вводя его в остальную часть вычислений, которая была дана нашему комбинированному действию.Таким образом, мы можем объединить вычисления в Cont:

computeFirst >>= \a ->
computeSecond >>= \b ->
return (a + b)

или, в более привычной записи do:

do a <- computeFirst
   b <- computeSecond
   return (a + b)

Затем мы можем запустить эти вычисления с помощьюrunCont - в большинстве случаев что-то вроде runCont foo id будет работать просто отлично, превращая foo с тем же результатом и типом конечного результата в его результат.

Пока все хорошо.Теперь давайте запутаемся.

wtf :: Cont String Int
wtf = Cont (\k -> "eek!")

aargh :: Cont String Int
aargh = do
  a <- return 1
  b <- wtf
  c <- return 2
  return (a + b + c)

Whздесь происходит ?!wtf - это Cont вычисление с конечным результатом String и результатом Int, но Int в поле зрения нет.

Что происходит, когда мы запускаем aargh, скажем, с runCont aargh show- т.е. запустить вычисление и show его Int результат как String для получения окончательного результата?

Мы получаем "eek!" назад.

Помните, как k это «остальная часть вычислений»?То, что мы сделали в wtf, - это хитро , а не , назовите это и вместо этого предоставьте наш собственный конечный результат - который затем становится, ну, наконец, окончательным!

Это всего лишь продолжение вещейсможет сделать.Что-то вроде Cont (\k -> k 1 + k 2) выполняет остальную часть вычисления, как если бы он возвратил 1, и снова , как если бы он возвратил 2, и складывает два окончательных результата вместе!Продолжения в основном позволяют выразить произвольно сложный нелокальный поток управления, делая их настолько мощными, насколько они сбивают с толку.Действительно, продолжения настолько общие, что в некотором смысле каждая монада является частным случаем Cont.В самом деле, вы можете думать о (>>=) в целом как о некоем стиле прохождения продолжения:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

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

Но я до сих пор не ответил на вопрос: что происходит с этим callCC?Ну, это вызывает функцию, которую вы даете с текущим продолжением.Но подождите секунду, разве это не то, что мы уже делали с Cont?Да, но сравните типы:

Cont   :: ((a -> r)        -> r)        -> Cont r a
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a

Да.Видите ли, проблема с Cont заключается в том, что мы не можем упорядочить действия из внутри функции, которую мы передаем - мы просто производим результат r в чистом виде.callCC позволяет получить доступ к продолжению, обойти его и просто смешать с вычислениями из Cont.Когда у нас есть

do a <- callCC (\cc -> ...)
   foo ...

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

Что касается таинственного b, то это просто потому, что вы можетеиспользуйте cc foo для вычисления любого типа, который вы хотите, поскольку он избегает нормального потока управления и, как я уже сказал, сразу же использует его как результат всегоcallCC (\cc -> ...).Таким образом, поскольку ему никогда не нужно создавать значение, ему может быть возвращено значение любого типа.Подлый!

Что приводит нас к фактической реализации:

callCC f = Cont (\k -> runCont (f (\a -> Cont (\_ -> k a))) k)

Сначала мы получим весь остаток вычислений и назовем его k.Но о чем эта f (\a -> Cont (\_ -> k a)) часть?Что ж, мы знаем, что f принимает значение типа (a -> Cont r b), и это то, чем является лямбда - функция, которая принимает значение для использования в результате callCC f, и возвращает вычисление Cont, которое игнорируетего продолжение и просто возвращает это значение через k - «остаток вычисления» с точки зрения callCC f.Итак, результат этого вызова f - это еще одно вычисление Cont, для которого нам потребуется предоставить продолжение, чтобы выполнить.Мы просто пропускаем то же самое продолжение снова, поскольку, если все пойдет нормально, мы хотим, чтобы все возвращаемое вычисление было нашим возвращаемым значением и продолжалось нормально.(Действительно, передача другого значения не имеет смысла смысла - это "вызов с текущим продолжением", а не "вызов с продолжением, отличным от того, с которым вы на самом деле меня запускаете").)

В общем, я надеюсь, что вы нашли это столь же поучительным, как и долго.Продолжения очень мощные, но может потребоваться много времени, чтобы понять, как они работают.Я предлагаю поиграть с Cont (который вам нужно будет назвать cont, чтобы заставить вещи работать с текущим mtl) и выяснить, как вы получаете результаты, которые вы делаете, чтобы почувствовать поток управления.

Рекомендованныйзакончились дальнейшие чтения по продолжениям:

10 голосов
/ 29 января 2012

call/cc тривиально реализовать.Сложной частью является настройка среды, в которой возможно для реализации.

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

newtype Cont r a = Cont { runCont :: (a -> r) -> r }

Как видите, действие монады Cont на самом деле является функцией, которая принимает продолжение (a -> r), передает результат a в продолжение,и возвращает окончательный результат r, который он просто передает своему вызывающему (т. е. действие монады Cont должно завершить вызов в продолжении).runCont просто извлекает его из оболочки нового типа - вы также можете думать о том, что он вызывает действие с некоторым определенным продолжением, как в runCont someAction someContinuation.

Затем мы можем превратить это в монаду:

instance Monad (Cont r) where
    return x = Cont $ \cc -> cc x
    (Cont f) (>>=) g = Cont $ \cc -> f (\r -> runCont (g r) cc)

Как видите, return просто получает продолжение cc и передает его значение в продолжение.(>>=) немного сложнее, он должен вызвать f с продолжением, которое затем вызывает g, возвращает действие, а затем передает внешнее продолжение этому новому действию.

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

-- Invoke a raw continuation with a given argument, throwing away our normal 
-- continuation
gotoContinuation :: (a -> r) -> a -> Cont r x
gotoContinuation continuation argument = Cont $ \_ -> continuation argument

-- Duplicate the current continuation; wrap one up in an easy-to-use action, and
-- the other stays the normal continuation for f
callCC f = Cont $ \cc -> runCont (f (gotoContinuation cc)) cc

Простой, нет?

В других языках, таких как Scheme, принцип тот же, хотя он может быть реализован как примитив компилятора;причина, по которой мы можем сделать это в Haskell, заключается в том, что передача продолжения была чем-то, что мы определили в Haskell, а не на более низком уровне времени выполнения.Но принцип тот же - сначала нужно иметь CPS, а затем call/cc - тривиальное применение этой модели исполнения.

6 голосов
/ 29 января 2012

Вы слышали сторону уравнения Хаскеля;Я дам вам ракетку / схему, и какой бы из них ни был наиболее полезным для вас, вы можете использовать его.

Мой ответ будет намного короче, потому что я думаю, что лучший источник, который я могу дать вам для реализации call / cc в простом инструменте оценки ракеток, исходит от PLAI Шрира Кришнамурти, особеннораздел 20. Я думал о включении соответствующей части переводчика - это на странице 205 - но после попытки переформатировать его несколько раз, я решил, что это будет иметь больше смысла в правильном месте на странице.

Опять же, я не пытаюсь объяснить идею, стоящую здесь за call / cc, просто укажу на работающую реализацию.Дайте мне знать, если у вас есть другие вопросы.

3 голосов
/ 30 января 2012

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

Чтобы понять, почему ваша попытка реализовать call/cc не удалась, вы должны понять, что такое стиль продолжения . Как только вы это поймете, все будет довольно просто:

  • call/cc не может быть реализовано в прямом стиле.
  • Однако тривиально реализовать в стиле передачи продолжения.

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

Любая программа может быть механически переведена в стиль передачи продолжения с помощью чего-то, что, соответственно, называется преобразованием CPS . Фактически многие системы Scheme работают так: программа анализируется, к ней применяется преобразование CPS, а затем дерево абстрактного синтаксиса CPS интерпретируется или транслируется в объектный код.

Вот как вы бы реализовали call/cc в стиле продолжения (используя continuation в качестве имени дополнительного аргумента для продолжения):

(define (call/cc-cps proc continuation)
  (proc continuation continuation))

Как вы должны видеть, (а) вы не можете реализовать это в прямом стиле (противоположность CPS), и (б) это тривиально в CPS. call/cc - это просто процедура, которая принимает другую процедуру в качестве аргумента и (обязательного) продолжения и вызывает эту процедуру с продолжением как в качестве аргумента, так и продолжения.

3 голосов
/ 29 января 2012

Риск быть неязыковым Я думаю, что в Smalltalk продолжения могут быть реализованы и поняты проще всего.Причина в том, что в Smalltalk стек выполнения формируется из обычных объектов, к которым можно обращаться и которыми можно манипулировать, как и с любым другим объектом.

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

Continuation>>initializeFromContext: aContext
    context := aContext.
    stream := WriteStream on: (Array new: 200).
    [ context notNil ] whileTrue: [
        stream nextPut: context.
        1 to: context class instSize do: [ :index |
            stream nextPut: (context instVarAt: index) ].
        1 to: context size do: [ :index |
            stream nextPut: (context at: index) ].
        context := context sender ].
    values := stream contents

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

Continuation>>value: anObject
    self terminate: thisContext.
    stream := values readStream.
    [ stream atEnd ] whileFalse: [
        context := stream next.
        1 to: context class instSize do: [ :index |
            context instVarAt: index put: stream next ].
        1 to: context size do: [ :index |
            context at: index put: stream next ] ]
    thisContext swapSender: values first.
    ^ anObject

С помощью этих двух методов мы можем легко построить callCC:

Continuation class>>callCC: aBlock
    ^ aBlock value: (self new initializeFromContext: thisContext sender)

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

Приведенный выше код взят из веб-платформы Seaside .Чтобы поиграть с кодом, вы можете использовать готовый дистрибутив и перейти к классам WAContinuation и WAContinuationTest.

...