Как применить 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 кажется подходящим, потому что он обеспечивает механизм продолжения выхода для использования с монадами продолжения, как указано в http://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-Cont.html#v:callCC.

Но я не могу понять, как применить callcc, и я не нашел подходящего источника, описывающего такое приложение.


person Community    schedule 30.09.2016    source источник
comment
Кроме того, настоятельно рекомендуется использовать что-то вроде R.curryN вместо ручного каррирования всех ваших функций ramdajs.com/docs/#curryN   -  person Jared Smith    schedule 30.09.2016
comment
@JaredSmith в этом вопросе нет тега Rambda. Кроме того, на чем основана ваша рекомендация?   -  person Mulan    schedule 30.09.2016
comment
@Jared Я считаю это наоборот: зачем мне использовать программное решение для карри, когда у меня есть стрелочные функции с небольшими синтаксическими издержками в моем наборе инструментов?   -  person    schedule 30.09.2016
comment
@naomik мог так же легко сказать _.curry, я просто предпочитаю ramda. Что касается почему, я лично предпочитаю гибкость, позволяющую передавать любое количество аргументов или по одному за раз.   -  person Jared Smith    schedule 30.09.2016
comment
@ftor, Джеймс Лонг сделал хорошую серию статей на эту тему: Что такое продолжение?, Реализация пошагового отладчика в JavaScript и Изучение продолжений: возобновляемые исключения. Он использует написанную им утилиту под названием Unwinder, которая в основном представляет собой просто исследование/демонстрацию того, как вызов/ cc можно реализовать на JS. Отказ от ответственности: все это очень сложно.   -  person Mulan    schedule 30.09.2016
comment
@JaredSmith в этом вопросе нет тега lodash или подчеркивания.   -  person Mulan    schedule 30.09.2016
comment
@ftor YMMV, в конце концов, написать свою собственную реализацию не так уж и сложно, если вы так качаетесь. Для тривиальных примеров или вашего собственного кода это почти наверняка не имеет значения, но если вы экспортируете API для пользы других, говоря что-то вроде f(3)('foo')(true), это менее идиоматично для JS, чем f(3, 'foo', true).   -  person Jared Smith    schedule 30.09.2016
comment
@naomik: Спасибо за ссылку на разматыватель. Выглядит многообещающе!   -  person    schedule 30.09.2016
comment
@JaredSmith Я уверен, что вы пытаетесь быть полезными, но ваши комментарии не по теме - это не вопрос о том, как и почему каррировать (или не каррировать) функции. Кроме того, ваши личные предпочтения не являются хорошим аргументом в поддержку.   -  person Mulan    schedule 30.09.2016
comment
@ftor ты будешь немного разочарован, я уверен. Unwinder включает своего рода этап предварительной компиляции. И автор рекомендует не использовать его в производственном коде. Я думаю, что есть просто некоторые функции, которые невозможно реализовать без поддержки самого языка.   -  person Mulan    schedule 30.09.2016
comment
@Jared Я думаю, что математические каррированные функции менее идиоматичны для ES5, но не для ES6 (=› стрелочные функции). Но это нормально, если мы не согласны по этому вопросу.   -  person    schedule 30.09.2016
comment
@ftor за написание кода для чтения, полностью с вами согласен. Я больше рассматривал это с точки зрения звонящего. Если звонящий вы, никаких проблем.   -  person Jared Smith    schedule 30.09.2016


Ответы (1)


Продолжения в двух словах

Продолжения — мощная абстракция. Позвольте мне подчеркнуть это. Продолжения — это чрезвычайно мощная абстракция. Почему продолжения так сильны? Это потому, что продолжение — это просто функция[1], а функции имеют опасное свойство вызываться. Подробнее об этом позже.

Итак, если продолжение — это просто функция, то почему оно такое особенное?

Да, продолжение — это просто функция. Однако то, что делает продолжение таким особенным, это то, что оно представляет. Продолжение представляет собой остальную часть вычислений (также называемую контекстом вычислений). Например, рассмотрим следующее выражение Scheme:

  (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 в Haskell:

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.

Мы поняли, что такое продолжения в Scheme. Мы поняли, как 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, есть различие между продолжениями и функциями. Тем не менее, даже если продолжение не является функцией, оно все равно похоже на функцию в том смысле, что оно вызывается.

person Aadit M Shah    schedule 03.10.2016
comment
Вы можете удалить, отредактировать и позже восстановить свой ответ, если вы случайно опубликовали его преждевременно. - person Bergi; 03.10.2016
comment
Я не могу дождаться, чтобы прочитать остальную часть вашего ответа, наверняка вы не только наткнетесь на вызываемые функции как на опасные, но и на продолжение, эквивалентное GOTO? - person Bergi; 03.10.2016
comment
Спасибо, что нашли время, @Aadit. Мне нравится ваш подход к объяснению call/cc с помощью примеров на Scheme, Haskell и Javascript. Я предполагаю, что эти вопросы и ответы являются хорошей отправной точкой для людей, которые хотят поиграть с cont/call/cc в Javascript, и если это приведет к нескольким сбоям браузеров из-за бесконечных циклов goto - ну, я согласен с этим. В любом случае, теперь, когда я лучше понимаю call/cc, интересно, что произойдет, если я передам cont в EitherT. Учитывая call/cc, этот набор инструментов кажется многообещающим для обработки асинхронных потоков управления в ленивом, одноадресном и монадическом стиле. - person ; 04.10.2016
comment
@Aadit Почему вы заключаете Cont в объект? Другими словами, почему Cont.of = x => new Cont(k => k(x)) вместо Cont.of = x => k => k(x)? Является ли продолжение в Haskell тоже завернутым? Или вы просто хотите создать тип продолжения, и единственный способ создать тип в Javascript состоит в назначении методов прототипу, поэтому вам нужен объект, который ссылается на этот прототип? - person ; 21.10.2016
comment
@ftor Это просто для того, чтобы мы могли использовать map, apply и bind как методы вместо функций. Это также позволяет нам реализовать map, apply и bind как общие функции. Например, вы можете реализовать общую карту как var map = f => x => x.map(f);. Эта функция map допускает частичное применение и работает для любого объекта, который реализует метод map. Следовательно, вы можете использовать его с продолжениями, а также с массивами и любым другим типом. - person Aadit M Shah; 22.10.2016
comment
Стоит отметить, что вам всегда нужна вспомогательная функция, такая как run, когда вы имеете дело с монадами, которые содержат функции в качестве значений, потому что возвращаемая лямбда-выражение заключено в экземпляр монады (тип Object в Javascript). То же самое, например, с монадой State. - person ; 17.11.2016