Правильная терминология для продолжений
В последнее время я размышлял о продолжениях, и я запутался в правильной терминологии. Здесь Габриэль Гонсалес говорит:
Продолжение Хаскелла имеет следующий тип:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
то есть. целая вещь (a -> r) -> r
является продолжением (без упаковки)
wikipedia, похоже, поддерживает эту идею, говоря
продолжение является абстрактным представлением состояния управления компьютерной программы.
Однако здесь авторы говорят, что
Продолжения - это функции, которые представляют собой "оставшиеся вычисления".
но это будет только (a->r)
часть типа Cont
. И это соответствует тому, что говорит Евгений Цин здесь:
вычисление (функция), которое требует функции продолжения в порядке для полной оценки.
Было бы много видеть эту функцию, поэтому дайте ей более интуитивное имя. Позволяет называть их функциями ожидания.
Я видел еще один учебник (Брайан Бекман и Эрик Мейер), где они называют все это (функция ожидания) наблюдаемым и функцию, необходимую для завершения наблюдателя.
- Что такое продолжение,
(a->r)->r
вещь или просто (a->r)
вещь (без упаковки)?
- Является ли формулировка наблюдаемого/наблюдателя правильной?
- Являются ли приведенные выше цитаты противоречивыми, существует ли общая истина?
Ответы
Ответ 1
Подпитывается чтением о продолжениях через Анджей Филински Декларативные континуумы и категориальная двойственность Я принимаю следующую терминологию и понимание.
Продолжение на значениях a
является "черной дырой, которая принимает значения a
". Вы можете видеть это как черный ящик с одной операцией - вы кормите его значением a
, а затем мир заканчивается. Локально по крайней мере.
Теперь предположим, что мы находимся в Haskell, и я требую, чтобы вы построили для меня функцию forall r . (a -> r) -> r
. Скажем, пока что a ~ Int
и он будет выглядеть как
f :: forall r . (Int -> r) -> r
f cont = _
где отверстие типа имеет такой контекст, как
r :: Type
cont :: Int -> r
-----------------
_ :: r
Ясно, что единственный способ, которым мы можем выполнить эти требования, - передать Int
в функцию cont
и вернуть его, после чего дальнейшие вычисления не могут произойти. Это моделирует идею "кормить Int до продолжения, а затем заканчивается мир".
Итак, я бы назвал функцию (a -> r)
продолжением до тех пор, пока он в контексте с фиксированным, но неизвестным r
и требованием вернуть это r
. Например, следующее не столько продолжение
forall r . (a -> r) -> (r, a)
поскольку нам явно разрешено передавать дополнительную информацию из нашей неудачной вселенной, чем позволяет только продолжение.
В "Наблюдаемый"
Я лично не являюсь поклонником "наблюдателя" / "наблюдаемой" терминологии. В этой терминологии мы могли бы написать
newtype Observable a = O { observe :: forall r . (a -> r) -> r }
так что мы имеем observe :: Observable a -> (a -> r) -> r
, который гарантирует, что ровно один a
будет передан "наблюдателю" a -> r
"наблюдение". Это дает очень оперативный взгляд на тип выше, в то время как cont
или даже условно названный Yoneda Identity
объясняет гораздо более декларативно то, что тип на самом деле.
Я думаю, что дело в том, чтобы каким-то образом скрыть сложность cont
за метафорой, чтобы сделать ее менее страшной для "среднего программиста", но это просто добавляет дополнительный слой метафоры для поведения, из-за которого возникает утечка. cont
и Yoneda Identity
точно объясните, что тип не наряжает.
Ответ 2
Что такое продолжение, (a- > r) → r thingy или просто (a- > r) вещь (без упаковки)?
Я бы сказал, что бит a -> r
является продолжением, а (a -> r) -> r
является "в стиле продолжения прохождения" или "является типом продолжения монады.
Я собираюсь пойти на длинное отступление от истории продолжений, которая не очень нравится в этом вопросе... так что будьте осторожны.
Я считаю, что первым опубликованным документом о продолжениях было "Продолжение: математическая семантика для обработки полных прыжков" Стрэчи и Уодсворта (хотя концепция была уже фольклором). Идея этой статьи я считаю довольно важной. Ранняя семантика для императивных программ пыталась моделировать команды как функции государственного трансформатора. Например, рассмотрим простой императивный язык, заданный следующим BNF
Command := set <Expression> to <Expression>
| skip
| <Command> ; <Command>
Expression := !<Expression>
| <Number>
| <Expression> + <Expression>
здесь мы используем выражения в качестве указателей. Простейшая денотационная функция интерпретирует состояние как функции от натуральных чисел до натуральных чисел:
S = N -> N
Мы можем интерпретировать выражения как функции от состояния к натуральным числам
E[[e : Expression]] : S -> N
и команды как преобразователи состояния.
C[[c : Command]] : S -> S
Эта денотационная семантика может быть проставлена довольно просто:
E[[n : Number]](s) = n
E[[a + b]](s) = E[[a]](s) + E[[b]](s)
E[[!e]](s) = s(E[[e]](s))
C[[skip]](s) = s
C[[set a to b]](s) = \n -> if n = E[[a]](s) then E[[b]](s) else s(n)
C[[c_1;c_2]](s) = (C[[c_2] . C[[c_1]])(s)
Как простая программа на этом языке может выглядеть как
set 0 to 1;
set 1 to (!0) + 1
который будет интерпретироваться как функция, которая превращает функцию состояния s
в новую функцию, которая похожа на s
, за исключением того, что она отображает 0
в 1
и 1
в 2
.
Все было хорошо и хорошо, но как вы справляетесь с ветвлением? Ну, если вы думаете об этом много, вы, вероятно, можете придумать способ обработки if
и циклов, которые идут точным числом раз... но как насчет общих while
циклов?
Стрэчи и Уодсворт показали нам, как это сделать. Прежде всего, они отметили, что эти "функции преобразователя состояния" были довольно важны и поэтому решили назвать их "продолжениями команд" или просто "продолжениями".
C = S -> S
Из этого они определили новую семантику, которую мы условно определим таким образом
C'[[c : Command]] : C -> C
C'[[c]](cont) = cont . C[[c]]
Что здесь происходит? Хорошо, заметим, что
C'[[c_1]](C[[c_2]]) = C[[c_1 ; c_2]]
и далее
C'[[c_1]](C'[[c_2]](cont) = C'[[c_1 ; c_2]](cont)
Вместо того, чтобы делать это таким образом, мы можем встроить определение
C'[[skip]](cont) = cont
C'[[set a to b]](cont) = cont . \s -> \n -> if n = E[[a]](s) then E[[b]](s) else s(n)
C'[[c_1 ; c_2]](cont) = C'[[c_1]](C'[[c_2]](cont)
Что это нам купило? Ну, способ интерпретировать while
, вот что!
Command := ... | while <Expression> do <Command> end
C'[[while e do c end]](cont) =
let loop = \s -> if E[[e]](s) = 0 then C'[[c]](loop)(s) else cont(s)
in loop
или, используя комбинатор фиксированных точек
C'[[while e do c end]](cont)
= Y (\f -> \s -> if E[[e]](s) = 0 then C'[[c]](f)(s) else cont(s))
В любом случае... это история и не особенно важна... за исключением того, что она показывала, как математически интерпретировать программы, и устанавливает язык "продолжения".
Кроме того, удивительно часто работает подход к денотационной семантике "1. определить новую семантическую функцию в терминах старой 2. inline 3. profit". Например, часто бывает полезно, чтобы ваш семантический домен представлял собой решетку (думаю, абстрактная интерпретация). Как вы это понимаете? Ну, один из вариантов заключается в том, чтобы взять poweret домена и ввести в это, интерпретируя ваши функции как одиночные. Если вы встроите эту конструкцию в Poweret, вы получите то, что может либо моделировать недетерминированность, либо, в случае абстрактной интерпретации, различные объемы информации о программе, отличные от точной уверенности в том, что она делает.
Затем выполнялась другая работа. Здесь я пропускаю многих великих людей, таких как лямбда-бумаги... Но, пожалуй, наиболее заметной была грандиозная статья Гриффина "Формула-как-тип Понятие управления", которая показала связь между продолжением стиля прохождения и классической логикой. Здесь подчеркивается связь между "продолжением" и "контекстом оценки"
То есть, E представляет остальную часть вычисления, которая еще предстоит сделать после оценки N. Контекст E называется продолжением (или контекстом управления) N в этой точке последовательности оценки. Обозначение контекстов оценки позволяет, как мы увидим ниже, краткую спецификацию оперативной семантики операторов, которые управляют продолжениями (действительно, это было его предполагаемое использование [3, 2, 4, 1]).
поясняющее, что "продолжение" представляет собой "только бит a -> r
"
Это все рассматривает вещи с точки зрения семантики и рассматривает продолжения как функции. Дело в том, что продолжения как функции дают вам больше энергии, чем вы получаете с чем-то вроде схемы callCC. Итак, еще одна перспектива продолжений заключается в том, что они являются переменными в программе, которые интернализуют стек вызовов. У Паригота была идея превратить переменные продолжения в отдельную синтаксическую категорию, ведущую к изящному исчислению лямбда-му в "λμ-Calculus: алгоритмическая интерпретация классического естественного дедукции".
Является ли наблюдаемая наблюдателем/наблюдателем правильная?
Я думаю, что это так, как это использует Эрик Мейджер. Это не стандартная терминология в академических PL.
Являются ли цитаты выше действительно противоречивыми, существует ли общая истина?
Давайте снова посмотрим на цитаты.
продолжение представляет собой абстрактное представление состояния управления компьютерной программой.
В моей интерпретации (которая, по моему мнению, довольно стандартная) продолжение моделирует то, что должна делать следующая программа. Я думаю, что википедия согласуется с этим.
Продолжение Хаскелла имеет следующий тип:
Это немного странно. Но, обратите внимание, что позже в сообщении Габриэль использует язык, который является более стандартным и поддерживает мое использование языка.
Это означает, что если мы имеем функцию с двумя продолжениями:
(a1 -> r) -> ((a2 -> r) -> r)
Ответ 3
Я предлагаю вспомнить соглашение о вызовах для C на платформах x86, из-за использования стека и регистров для передачи аргументов. Это будет очень полезно для понимания абстракции.
Предположим, что функция f
вызывает функцию g
и передает ей 0
. Это будет выглядеть так:
mov eax, 0
call g -- now eax is the first argument,
-- and the stack has the address of return point, f'
g: -- here goes g that uses eax to compute the return value
mov eax,1 -- which by calling convention is placed in eax
ret -- get the return point, f', off the stack, and jump there
f': ...
Понимаете, размещение точки возврата f'
в стеке совпадает с передачей указателя функции в качестве одного из аргументов, а затем возврат совпадает с вызовом данной функции и передает ей значение. Таким образом, с точки зрения g
точка возврата на f
выглядит как функция одного аргумента, f' :: a -> r
. Как вы понимаете, состояние стека полностью захватывает состояние вычисления f
, и для этого требуется a
от g
.
В то же время в точке g
он называется функцией, которая принимает функцию одного аргумента (мы помещаем указатель этой функции в стек), который в конечном итоге вычислит значение типа r
, что для вычисления был рассчитан код из f':
, поэтому тип становится g :: (a->r)->r
.
Так как f'
задано значение типа a
из "где-то", f'
можно рассматривать как наблюдателя g
- это, наоборот, наблюдаемое.
Это предназначено только для того, чтобы дать базовую идею и как-то связать с миром, который вы, вероятно, уже знаете. Магия продолжений позволяет делать больше трюков, чем просто преобразовывать "простые" вычисления в вычисления с продолжениями.
Ответ 4
Когда мы говорим о продолжении, мы подразумеваем ту часть, которая позволяет продолжить вычисление результата.
Операция в Continued Monad аналогична неполной функции, поэтому она ждет другую функцию для ее завершения. Несмотря на то, что Continued Monad сама по себе является правильной конструкцией, которая может быть использована для завершения другой Continued Monad, это то, что делает оператор привязки (>>=
) для Cont Monad.
При написании кода, который включает callCC
или Call with Current Continuation, вы передаете текущую Cont Monad в другую Cont Monad, чтобы второй мог ее использовать. Например, это может привести к преждевременному завершению выполнения, вызвав первую Cont Monad, и оттуда цикл может либо повториться, либо расходиться в другую Contination Monad.
Часть, которая является продолжением, отличается от той перспективы, которую вы используете. По моему личному мнению, лучший способ описать продолжение относится к другой конструкции.
Итак, если мы вернемся к нашему примеру двух взаимодействующих друг с другом монадов, то с точки зрения первой Монады продолжением будет (a -> r) -> r
(потому что это разворачиваемый тип первой Монады) и с точки зрения второй Монады продолжение - это (a -> r)
(потому что это разворачиваемый тип первой монады, когда a
заменяется на (a -> r)
).