Существует ли реальная применимость для продолжения монады вне академического использования?

(позже посетители: два ответа на этот вопрос дают отличное представление, если вы заинтересованы, вы, вероятно, должны прочитать их оба, я мог бы только исключить одно из ограничений SO)

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

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

Здесь версия F # продолжения, взятая из этого сообщения SO:

type ContinuationMonad() =
    member this.Bind (m, f) = fun c -> m (fun a -> f a c)
    member this.Return x = fun k -> k x

let cont = ContinuationMonad()

Это просто академический интерес, например, чтобы помочь понять монады или сборщики вычислений? Или существует какая-то реальная применимость, добавленная безопасность типа или она обходит типичные проблемы программирования, которые трудно решить иначе?

То есть, продолжение монады с вызовом /cc от Райана Райли показывает, что сложно обрабатывать исключения, но это не объясняет какую проблему он пытается решить, и примеры не показывают, зачем ему нужна именно эта монада. По общему признанию, я просто не понимаю, что он делает, но это может быть сокровищница!

(Примечание: Мне не интересно понимать, как работает монада продолжения, я думаю, что у меня есть справедливое представление об этом, я просто не понимаю, какую проблему программирования он решает.)

Ответ 1

Материал "матери всех монадов" не является чисто академическим. Дан Пипони упоминает Анджей Филински Представление Monads - довольно хорошая статья. Результатом этого является то, что если ваш язык имеет разграниченные продолжения (или может имитировать их с помощью call/cc и одной части изменяемого состояния), тогда вы можете прозрачно добавлять какой-либо монадический эффект для любого кода. Другими словами, если у вас есть разграниченные продолжения и никакие другие побочные эффекты, вы можете реализовать (глобальное) изменяемое состояние или исключения или отказаться от детерминизма или сотрудничества concurrency. Вы можете сделать каждый из них, просто определив несколько простых функций. Нет глобальной трансформации или чего-то еще. Кроме того, вы платите только за побочные эффекты при их использовании. Оказывается, Schemers были совершенно правы, когда call/cc был очень выразительным.

Если ваш язык не имеет разграниченных продолжений, вы можете получить их через продолжённую монаду (или, лучше, монашескую продолженную монаду). Конечно, если вы собираетесь писать в монадическом стиле в любом случае - ndash; который является глобальным преобразованием – почему бы просто не использовать желаемую монаду с самого начала? Однако для Haskellers это обычно то, что мы делаем, однако по-прежнему есть преимущества от использования продолжения монады во многих случаях (хотя и скрытой). Хорошим примером является монада Maybe/Option, которая похожа на исключения, кроме только одного типа исключения. В принципе, эта монада захватывает шаблон возврата "кода ошибки" и проверяет его после каждого вызова функции. И это именно то, что делает типичное определение, за исключением "вызова функции", я имел в виду каждый (монадический) шаг вычисления. Достаточно сказать, что это довольно неэффективно, особенно когда в подавляющем большинстве случаев ошибок нет. Если вы отражаете Maybe в продолжении монады, хотя, хотя вы должны оплачивать стоимость кода CPSed (который GHC Haskell отлично справляется с этим), вы платите только за "код ошибки" в местах, где это имеет значение, т.е. catch. В Haskell монада Codensity, чем упомянутый данидиад, является лучшим выбором, потому что последнее, чего хочет Haskellers, состоит в том, чтобы сделать так, чтобы произвольные эффекты могли быть прозрачно чередующимися в их коде.

Как упоминалось также данидиаз, многие монады легче или эффективнее реализуются с использованием, по существу, продолжения монады или некоторого варианта. Обратный поиск - один из примеров. Хотя это не самая новая вещь в обратном направлении, одна из моих любимых работ, которая использовала ее, была типизированные логические переменные в Haskell. Используемые в нем методы также использовались на языке описания проводного оборудования. Также из Koen Claesson Бедный человек Concurrency Монада. Более современное использование идей в этом примере включает в себя: монаду для детерминированного parallelism в Haskell Monad для детерминированных Parallelism и масштабируемых модулей ввода /O Объединение событий и потоков для масштабируемых сетевых сервисов. Я уверен, что могу найти похожие методы, используемые в Scala. Если он не был предоставлен, вы можете использовать монаду продолжения для реализации асинхронных рабочих процессов в F #. Фактически, Don Syme ссылки точно такие же документы, на которые я только что ссылался. Если вы можете сериализовать функции, но не иметь продолжений, вы можете использовать монаду продолжения, чтобы получить их, и сделать сериализованный тип продолжения веб-программирования, популярный в таких системах, как Seaside. Даже без сериализуемых продолжений вы можете использовать шаблон (по сути, такой же, как и для aync), чтобы, по крайней мере, избегать обратных вызовов при сохранении продолжений локально и только отправке ключа.

В конечном счете, относительно мало людей за пределами Haskellers используют монады в любой степени, и, как я уже упоминал ранее, Haskellers, как правило, хотят использовать более прокручиваемые монады, чем монады продолжения, хотя они довольно часто используют их внутри. Тем не менее, продолжения монады или продолжение монады, как и вещи, особенно для асинхронного программирования, становятся все менее необычными. Поскольку С#, F #, Scala, Swift и даже Java начинают включать поддержку монадического или, по крайней мере, монадического программирования, эти идеи станут более широко использоваться. Если бы разработчики Node были более знакомы с этим, возможно, они поняли бы, что у вас может быть ваш торт и есть его тоже в отношении программирования, управляемого событиями.

Ответ 2

Чтобы обеспечить более прямой ответ на F # (хотя Derek уже рассмотрел это тоже), монада продолжения в значительной степени отражает суть работы асинхронных рабочих процессов.

Монада продолжения - это функция, которая при задании продолжения в конечном итоге вызывает продолжение с результатом (он никогда не может называть его или он может называть его повторно):

type Cont<'T> = ('T -> unit) -> unit

Асинхронные вычисления F # немного сложнее - они принимают продолжение (в случае успеха), исключения и отмены, а также включают токен отмены. Используя немного упрощенное определение, используется базовая библиотека F # (см. полное определение здесь):

type AsyncParams =
    { token : CancellationToken
      econt : exn -> unit
      ccont : exn -> unit } 

type Async<'T> = ('T -> unit) * AsyncParams -> unit

Как вы можете видеть, если вы игнорируете AsyncParams, это в значительной степени продолжение монады. В F # я считаю, что "классические" монады более полезны как вдохновение, чем как механизм прямой реализации. Здесь монада продолжения предоставляет полезную модель обработки определенных видов вычислений - и со многими дополнительными асинхронными аспектами основная идея может использоваться для реализации асинхронных вычислений.

Я думаю, что это совсем не так, как монады используются в классических академических работах или в Haskell, где они, как правило, используются "как есть" и, возможно, составлены разными способами для создания более сложных монадов, которые захватывают более сложное поведение.

Это может быть только мое личное мнение, но я бы сказал, что монада продолжения практически не полезна сама по себе, но она является основой для некоторых очень практических идей. (Так же, как исчисление лямбда на самом деле не очень полезно само по себе, но его можно рассматривать как вдохновение для хороших практических языков!)

Ответ 3

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

type 'a Tree = 
| Node of 'a * 'a Tree * 'a Tree
| Empty

здесь один способ записать снизу вверх по дереву:

let rec fold e f t = cont {
    match t with
    | Node(a,t1,t2) ->
        let! r1 = fold e f t1
        let! r2 = fold e f t2
        return f a r1 r2
    | Empty -> return e
}

Это явно аналогично наивной складке:

let rec fold e f t =
    match t with
    | Node(a,t1,t2) ->
        let r1 = fold e f t1
        let r2 = fold e f t2
        f a r1 r2
    | Empty -> return e

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

let rec fold e f t k = 
    match t with
    | Node(a,t1,t2) -> 
        fold e f t1 (fun r1 ->
        fold e f t2 (fun r2 ->
        k (f r1 r2)))
    | Empty -> k e

Обратите внимание, что для того, чтобы это сработало, вам нужно изменить свое определение ContinuationMonad, чтобы включить

member this.Delay f v = f () v