Как применить callcc, чтобы обеспечить механизм продолжения escape для использования с монадой продолжения
Я пытаюсь реализовать монаду продолжения в Javascript для обработки стиля передачи продолжения и асинхронных потоков управления. Вот моя монада продолжения обучения:
// auxiliary functions
const log = prefix => x => console.log(prefix, x);
const addk = x => y => k => setTimeout((x, y) => k(x + y), 0, x, y);
const inck = x => k => setTimeout(x => k(x + 1), 0, x);
const sqr = x => x * x;
// continuation monad
const cont = {
of: x => k => k(x),
map: ftor => f => k => ftor(x => k(f(x))),
ap: ftor => gtor => k => ftor(x => gtor(f => k(f(x)))),
chain: ftor => mf => k => ftor(x => mf(x) (k)),
callcc: f => k => f(x => () => k(x)) (k)
};
// map a normal, unary function with an asynchronous function:
cont.map(inck(9)) (sqr) (log("map"));
// chain two asynchronous CPS computations with an asynchronous binary CPS function
const comp1 = cont.map(inck(4)) (sqr);
const comp2 = cont.map(inck(9)) (sqr);
cont.chain(comp1) (x => cont.chain(comp2) (y => addk(x) (y))) (log("chain"));
Помимо cont.ap
, чья выгода не раскрывается мне, все работает отлично.
Теперь я хотел бы подражать throw
/ catch
Механизм синхронного управления потоками в Javascript. callcc
кажется подходящим, потому что он обеспечивает механизм продолжения Escape для использования с монадами продолжения, как указано на http://hackage.haskell.org/.
Но я не могу понять, как подать заявку callcc
и я не нашел подходящего источника, который описывает такое приложение.
1 ответ
Продолжения в двух словах
Продолжения - это мощная абстракция. Позвольте мне подчеркнуть это. Продолжения - чрезвычайно мощная абстракция. Почему продолжения такие мощные? Это потому, что продолжение - это просто функция[1] и "функции имеют опасное свойство быть вызываемым". Подробнее об этом позже.
Итак, если продолжение - это просто функция, то почему оно такое особенное?
Да, продолжение - это просто функция. Однако то, что делает продолжение таким особенным, это то, что оно представляет. Продолжение представляет "остальную часть вычислений" (или контекст вычислений). Например, рассмотрим следующее выражение схемы:
(add1 (* 3 x))
; |_____|
; |
; computation
(add1 [])
; |_______|
; |
; context
Здесь вычисление (* 3 x)
имеет контекст (add1 [])
где []
представляет собой дыру. Отверстие может быть закрыто результатом вычисления. Это написано как (add1 [result])
для некоторых result
, Продолжение - это просто представление контекста. Например, функция (lambda (result) (add1 result))
представляет контекст (add1 [])
,
С другой стороны, вычисление (* 3 x)
также может быть представлен как функция. Это представлено как функция (lambda (context) (context (* 3 x)))
где context
является продолжением, представляющим контекст вычисления. Следует отметить, что Cont
Тип в Haskell представляет само вычисление, а не его контекст.
Хорошо, но что делает продолжения такими мощными?
Как я уже говорил, продолжение - это просто функция, и "функции имеют опасное свойство быть вызываемым". В частности, функция может вызываться не только один раз, но и произвольно много раз или даже никогда вообще. Это то, что делает продолжения такими мощными.
Например, рассмотрим вышеупомянутые вычисления(* 3 x)
представляется как функция:
(lambda (context)
(context (* 3 x)))
Что делать, если мы подали заявкуcontext
больше чем единожды? Мы могли бы использовать это, чтобы удвоить результат следующим образом:
(lambda (context)
(+
(context (* 3 x))
(context (* 3 x))))
Если даноcontext
являетсяadd1
тогда это даст ответ (* 2 (add1 (* 3 x)))
,
С другой стороны, что, если мы никогда не обращалисьcontext
? Мы могли бы замкнуть оценку:
(lambda (context)
(* 3 x))
Это именно то, чтоcall/cc
делает. Это замыкает оценку, игнорируя контекст и просто возвращая ответ. Например, рассмотрим:
(call/cc (lambda (short-circuit)
(add1 (short-circuit (* 3 x)))))
Здесь вычисление(* 3 x)
имеет контекстadd1
, Тем не менее, мы замкнули вычисления, применив контекст call/cc
(т.е. short-circuit
) к результату расчета. Следовательно, мы игнорировали исходный контекст (т.е. add1
) и вернул ответ.
Какcall/cc
реализованы?
Теперь, когда мы понимаем продолжения, давайте посмотрим на определениеcallCC
в Хаскеле:
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
-- |___________________________|
-- |
-- f
callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
-- __|___ |______________________|
-- | | |
-- (a -> r) short-circuit
Следует отметить, чтоk
текущее продолжение (т.е. продолжение всего выражения). Функцияf
является единственным входом вcallCC
, ВозвращаетCont r a
который представляет все вычисления, которые будут выполнены. Мы применяем это к k
чтобы получить результат вычисления.
Однако внутри вычисления мы можем назвать short-circuit
всякий раз, когда мы хотим замкнуть оценку. Эта функция принимает результат и возвращает вычисление, которое игнорирует его контекст и применяет текущее продолжение k
к результату, тем самым закорачивая оценку.
Собираем все это вместе в JavaScript.
Мы поняли, что продолжения в Схеме. Мы поняли какcallCC
работает в Haskell. Давайте использовать наши обретенные знания для реализации продолжений иcallCC
в JavaScript:
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = prefix => x => console.log(prefix, x);
var add1 = x => Cont.of(x + 1);
callCC(short_circuit => short_circuit(15).bind(add1)).runCont(log("short"));
callCC(short_circuit => Cont.of(15).bind(add1)).runCont(log("no short"));
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
Осторожно,callCC
может быть использован для реализацииgoto
,
СилаcallCC
позволяет создавать произвольные структуры потока управления, такие какthrow
, сопрограммы и даже goto
как можно увидеть здесь:
var Cont = defclass({
constructor: function (runCont) {
this.runCont = runCont;
},
map: function (f) {
return new Cont(k => this.runCont(x => k(f(x))));
},
apply: function (g) {
return new Cont(k => this.runCont(f => g.runCont(x => k(f(x)))));
},
bind: function (f) {
return new Cont(k => this.runCont(x => f(x).runCont(k)));
}
});
Cont.of = x => new Cont(k => k(x));
var callCC = f => new Cont(k => f(x => new Cont(_ => k(x))).runCont(k));
var log = (x, ms) => new Cont(k => setTimeout(_ => k(console.log(x)), ms));
var omega = x => x(x); // This is a very dangerous function. Run `omega(omega)`.
callCC(omega).bind(cc => log("loop", 1000).bind(_ => cc(cc))).runCont(x => x);
function defclass(prototype) {
var constructor = prototype.constructor;
constructor.prototype = prototype;
return constructor;
}
Этот код эквивалентен:
forever:
delay(1000);
print("loop");
goto forever;
Следовательно, вы должны быть осторожны при работе с продолжениями.
[1] Продолжения обычно реализуются с использованием первоклассных функций. Однако в языках с первоклассными продолжениями, такими как Scheme, существует различие между продолжениями и функциями. Тем не менее, даже если продолжение не является функцией, оно все равно похоже на функцию, которую можно вызвать.