Монада на простом английском? (Для программиста ООП без фона FP)

С точки зрения того, что программист ООП поймет (без какой-либо функциональной основы программирования), что такое монада?

Какую проблему он решает и какие места он использует чаще всего?

РЕДАКТИРОВАТЬ:

Чтобы прояснить то понимание, которое я искал, допустим, вы конвертировали приложение FP с монадами в приложение ООП. Что бы вы сделали, чтобы перенести обязанности монад в приложение ООП?

23 ответа

ОБНОВЛЕНИЕ: Этот вопрос был предметом очень длинной серии блогов, которую вы можете прочитать на Monads - спасибо за отличный вопрос!

С точки зрения того, что программист ООП поймет (без какой-либо функциональной основы программирования), что такое монада?

Монада - это "усилитель" типов, который подчиняется определенным правилам и в котором предусмотрены определенные операции.

Во-первых, что такое "усилитель типов"? Под этим я подразумеваю некоторую систему, которая позволяет вам брать тип и превращать его в более специальный тип. Например, в C# рассмотрим Nullable<T>, Это усилитель типов. Это позволяет вам взять тип, скажем int и добавить новую возможность для этого типа, а именно, что теперь он может быть нулевым, если не мог раньше.

В качестве второго примера рассмотрим IEnumerable<T>, Это усилитель типов. Это позволяет вам взять тип, скажем, string и добавьте к этому типу новую возможность, а именно то, что теперь вы можете создать последовательность строк из любого числа отдельных строк.

Каковы "определенные правила"? Вкратце, существует разумный способ для функций базового типа работать с усиленным типом таким образом, чтобы они следовали обычным правилам функциональной композиции. Например, если у вас есть функция целых чисел, скажем

int M(int x) { return x + N(x * 2); }

тогда соответствующая функция на Nullable<int> может заставить всех операторов и звонки там работать вместе "так же, как они делали раньше.

(Это невероятно расплывчато и неточно; вы попросили объяснения, в котором ничего не говорилось о знании функциональной композиции.)

Каковы "операции"?

  1. Существует операция "unit" (иногда вызывающая путаницу, называемая операцией "return"), которая берет значение из простого типа и создает эквивалентное монадическое значение. Это, по сути, дает возможность взять значение неусиленного типа и превратить его в значение усиленного типа. Это может быть реализовано как конструктор на языке ОО.

  2. Существует операция "связать", которая принимает монадическое значение и функцию, которая может преобразовать значение и возвращает новое монадическое значение. Связывание является ключевой операцией, которая определяет семантику монады. Это позволяет нам преобразовывать операции над неусиленным типом в операции над усиленным типом, которые подчиняются правилам функциональной композиции, упомянутым ранее.

  3. Часто есть способ вернуть неусиленный тип из усиленного. Строго говоря, для этой операции не обязательно иметь монаду. (Хотя это необходимо, если вы хотите иметь комонаду. Мы не будем обсуждать это далее в этой статье.)

Опять возьми Nullable<T> В качестве примера. Вы можете включить int в Nullable<int> с конструктором. Компилятор C# позаботится о наиболее обнуляемом "поднятии" за вас, но если этого не произойдет, преобразование подъема будет простым: операция, скажем,

int M(int x) { whatever }

превращается в

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

И превращая Nullable<int> обратно в int сделано с Value имущество.

Это преобразование функции является ключевым битом. Обратите внимание на то, как фактическая семантика операции Nullable - что операция на null размножает null - захвачен в трансформации. Мы можем обобщить это.

Предположим, у вас есть функция от int в int как наш оригинал M, Вы можете легко превратить это в функцию, которая принимает int и возвращает Nullable<int> потому что вы можете просто запустить результат через конструктор Nullable. Теперь предположим, что у вас есть метод высшего порядка:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Видишь, что ты можешь с этим сделать? Любой метод, который принимает int и возвращает int или занимает int и возвращает Nullable<int> теперь можно применить к нему семантику, допускающую обнуление.

Кроме того: предположим, у вас есть два метода

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

а ты хочешь их составить

Nullable<int> Z(int s) { return X(Y(s)); }

То есть, Z это состав X а также Y, Но вы не можете сделать это, потому что X занимает int, а также Y возвращает Nullable<int>, Но так как у вас есть операция "связать", вы можете сделать эту работу:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

Операция связывания с монадой - это то, что заставляет работу функций на усиленных типах работать. "Правила", о которых я писал выше, заключаются в том, что монада сохраняет правила нормальной композиции функций; что сочинение с использованием тождественных функций приводит к исходной функции, эта композиция является ассоциативной и так далее.

В C# "Bind" называется "SelectMany". Посмотрите, как это работает на монаде последовательности. Нам нужно иметь две вещи: превратить значение в последовательность и связать операции над последовательностями. В качестве бонуса у нас также есть "превратить последовательность обратно в значение". Эти операции:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

Правило обнуляемой монады заключалось в том, чтобы "объединить две функции, которые производят обнуляемые вместе, проверить, не приводит ли внутренняя к нулю; если это так, вывести нулевое, если нет, то вызвать внешнюю с результатом". Это желаемая семантика обнуляемого.

Правило монады последовательностей состоит в том, чтобы "объединить две функции, которые производят последовательности, применить внешнюю функцию к каждому элементу, созданному внутренней функцией, и затем объединить все результирующие последовательности вместе". Фундаментальная семантика монад отражена в Bind / SelectMany методы; это метод, который говорит вам, что на самом деле означает монада.

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

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

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

Какую проблему он решает и какие места он использует чаще всего?

Это все равно, что спросить: "Какие проблемы решает шаблон синглтона?", Но я попробую.

Монады обычно используются для решения таких проблем, как:

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

C# использует монады в своем дизайне. Как уже упоминалось, обнуляемый шаблон очень похож на "возможно, монаду". LINQ полностью построен из монад; SelectMany Метод заключается в том, что делает семантическую работу композиции операций. (Эрик Мейер любит указывать, что каждая функция LINQ может быть реализована SelectMany; все остальное просто удобство.)

Чтобы прояснить то понимание, которое я искал, допустим, вы конвертировали приложение FP с монадами в приложение ООП. Что бы вы сделали, чтобы перенести обязанности монад в приложение ООП?

Большинство языков ООП не имеют достаточно богатой системы типов, чтобы напрямую представлять сам образец монады; вам нужна система типов, которая поддерживает типы более высоких типов, чем универсальные типы. Так что я бы не стал это делать. Скорее, я бы реализовал универсальные типы, которые представляют каждую монаду, и реализовал методы, которые представляют три необходимые вам операции: преобразование значения в усиленное значение, (возможно) преобразование усиленного значения в значение и преобразование функции с неусиленными значениями в функция на усиленные значения.

Хорошее начало - как мы реализовали LINQ в C#. Изучите SelectMany Способ; это ключ к пониманию того, как работает монада последовательностей в C#. Это очень простой метод, но очень мощный!


Предложено, дальнейшее чтение:

  1. Для более глубокого и теоретически обоснованного объяснения монад в C# я настоятельно рекомендую мою ( Eric Lippert) статью коллеги Уэса Дайера на эту тему. Эта статья - то, что объяснило мне монады, когда они наконец "щелкнули" для меня.
  2. Хорошая иллюстрация того, почему вам может понадобиться монада (использует Haskell в своих примерах).
  3. Вроде, "перевод" предыдущей статьи на JavaScript.

Зачем нам нужны монады?

  1. Мы хотим программировать только с использованием функций. ("Функциональное программирование" в конце концов -FP).
  2. Тогда у нас есть первая большая проблема. Это программа:

    f(x) = 2 * x

    g(x,y) = x / y

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

    Решение: составить функции. Если хочешь первым g а потом f, просто пиши f(g(x,y)), Да, но...

  3. Больше проблем: некоторые функции могут не работать (т.е. g(2,0), разделите на 0). У нас нет "исключений" в ФП. Как мы это решаем?

    Решение: давайте позволим функциям возвращать два вида вещей: вместо g : Real,Real -> Real (функция из двух реалов в реальный), давайте позволим g : Real,Real -> Real | Nothing (функция из двух реалов в (реальные или ничего)).

  4. Но функции должны (быть проще) возвращать только одну вещь.

    Решение: давайте создадим новый тип данных, которые должны быть возвращены, "тип бокса", который может быть реальным или просто ничем. Следовательно, мы можем иметь g : Real,Real -> Maybe Real, Да, но...

  5. Что происходит сейчас с f(g(x,y))? f не готов потреблять Maybe Real, И мы не хотим менять каждую функцию, с которой мы могли бы соединиться g потреблять Maybe Real,

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

    В нашем случае: g >>= f (Подключение / сочинить g в f). Мы хотим >>= получить gвывод, проверьте его и, если это Nothing просто не звони f и вернуться Nothing; или наоборот, распаковать в штучной упаковке Real и кормить f с этим. (Этот алгоритм является просто реализацией >>= для Maybe тип).

  6. Возникают многие другие проблемы, которые могут быть решены с использованием этого же шаблона: 1. Используйте "коробку" для кодификации / хранения различных значений / значений и используйте такие функции, как g которые возвращают эти "коробочные значения". 2. Есть композиторы / линкеры g >>= f чтобы помочь соединению gвыход в fвход, поэтому нам не нужно менять f совсем.

  7. Замечательные проблемы, которые могут быть решены с помощью этой техники:

    • имея глобальное состояние, которое каждая функция в последовательности функций ("программа") может совместно использовать: решение StateMonad,

    • Нам не нравятся "нечистые функции": функции, которые дают разные выходные данные для одного и того же ввода. Поэтому давайте отметим эти функции, заставив их возвращать теговое / коробочное значение: IO монада.

Полное счастье!!!!

Я бы сказал, что ближайшая аналогия с монадами - это " шаблон команд".

В шаблоне команды вы помещаете обычное выражение или выражение в объект команды. Командный объект предоставляет метод execute, который выполняет упакованный оператор. Таким образом, оператор превращается в объекты первого класса, которые могут передаваться и выполняться по желанию. Команды могут быть составлены так, что вы можете создать программный объект путем объединения и вложения командных объектов.

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

Шаблон команды может использоваться для добавления (или удаления) языковых функций, которые не поддерживаются языком хоста. Например, в гипотетическом языке ОО без исключений вы можете добавить семантику исключений, предоставляя командам методы try и throw. Когда команда вызывает throw, вызывающий возвращается к списку (или дереву) команд до последнего вызова "try". И наоборот, вы можете удалить семантику исключений из языка (если вы считаете, что исключения плохие), перехватывая все исключения, создаваемые каждой отдельной командой, и превращая их в коды ошибок, которые затем передаются следующей команде.

Еще более причудливая семантика выполнения, такая как транзакции, недетерминированное выполнение или продолжения, может быть реализована таким образом на языке, который не поддерживает его изначально. Это довольно мощный паттерн, если подумать.

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

С точки зрения того, что программист ООП поймет (без какой-либо функциональной основы программирования), что такое монада?

Какую проблему он решает и в каких местах он чаще всего используется?

С точки зрения ОО-программирования, монада - это интерфейс (или, скорее, миксин), параметризованный типом, с двумя методами, return а также bind которые описывают:

  • Как ввести значение, чтобы получить монадическое значение этого типа введенного значения;
  • Как использовать функцию, которая делает монадическое значение не немонадным, на монадическом значении.

Решаемая проблема - это проблема того же типа, которую вы ожидаете от любого интерфейса, а именно: "У меня есть куча разных классов, которые делают разные вещи, но, кажется, делают эти разные вещи способом, который имеет основное сходство. Могу ли я описать это сходство между ними, даже если сами классы на самом деле не являются подтипами чего-то более близкого, чем сам класс "Объект"?"

Более конкретно, Monad "интерфейс" похож на IEnumerator или же IIterator в том, что он принимает тип, который сам принимает тип. Основная "точка" Monad хотя он может соединять операции, основанные на внутреннем типе, даже до того, чтобы иметь новый "внутренний тип", сохраняя - или даже улучшая - информационную структуру основного класса.

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

Вот краткое и точное определение, слегка перефразированное:

Монада (в информатике) - это карта, которая:

  • отправляет каждый тип X некоторого данного языка программирования в новый тип T(X) (называется "тип T вычисления со значениями в X ");

  • оснащен правилом для составления двух функций вида f:X->T(Y) а также g:Y->T(Z) к функции g∘f:X->T(Z);

  • таким образом, что является ассоциативным в очевидном смысле и унитальным по отношению к данной единичной функции, называемой pure_X:X->T(X), чтобы считаться принятием значения в чистом вычислении, которое просто возвращает это значение.

Итак, простыми словами, монада является правилом для перехода от любого типа X к другому типу T(X) и правило, чтобы перейти от двух функций f:X->T(Y) а также g:Y->T(Z) (что вы хотели бы написать, но не можете) для новой функции h:X->T(Z), Что, однако, не является композицией в строгом математическом смысле. Мы в основном "сгибаем" состав функции или переопределяем, как составляются функции.

Кроме того, мы требуем, чтобы правило сочинения монады удовлетворяло "очевидным" математическим аксиомам:

  • Ассоциативность: составление f с g а затем с h (снаружи) должен быть таким же, как сочинение g с h а затем с f (изнутри).
  • Unital собственность: составление f с тождественной функцией по обе стороны должен дать f,

Опять же, простыми словами, мы не можем просто сойти с ума, переопределив нашу композицию функций так, как нам нравится:

  • Сначала нам нужна ассоциативность, чтобы иметь возможность составлять несколько функций подряд, например f(g(h(k(x)))и не беспокоиться об указании порядка составления пар функций. Поскольку правило монады только предписывает, как составлять пару функций без этой аксиомы, нам нужно знать, какая пара составлена ​​первой и так далее. (Обратите внимание, что отличается от свойства коммутативности, что f состоит из g были такими же, как g состоит из f, что не обязательно).
  • И, во-вторых, нам нужно свойство унитала, то есть просто сказать, что идентичности складываются так, как мы их ожидаем. Таким образом, мы можем безопасно реорганизовывать функции всякий раз, когда эти идентичности могут быть извлечены.

Итак, еще раз вкратце: монада - это правило расширения типа и составления функций, удовлетворяющих двум аксиомам - ассоциативности и унитальному свойству.

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

По сути, это в двух словах.


Будучи профессиональным математиком, я предпочитаю избегать звонков h "композиция" f а также g, Потому что математически это не так. Называя это "композиция" неправильно предполагает, что h это истинная математическая композиция, которой это не так. Это даже не однозначно определяется f а также g, Вместо этого это результат нового "правила составления" нашей монады. Который может полностью отличаться от фактического математического состава, даже если последний существует!


Монада не функтор! Функтор F это правило, чтобы перейти от типа X печатать F(X) и функции (морфизм) между типами X а также Y к функциям между F(X) а также F(Y) (отправка объектов в объекты и их морфизмы в морфизмы в теории категорий). Вместо этого монада посылает пару функций f а также g к новому h,


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

Исключение как примеры монады

Предположим, мы хотим составить две функции:

f: x -> 1 / x
g: y -> 2 * y

Но f(0) не определено, поэтому исключение e брошен Тогда как вы можете определить композиционную ценность g(f(0))? Разумеется, снова бросьте исключение! Может быть то же самое e, Может быть, новое обновленное исключение e1,

Что именно здесь происходит? Во-первых, нам нужны новые значения исключений (разные или одинаковые). Вы можете позвонить им nothing или же null или что-то еще, но суть остается той же - они должны быть новыми ценностями, например, это не должно быть number в нашем примере здесь. Я предпочитаю не называть их null чтобы избежать путаницы с тем, как null может быть реализован на любом конкретном языке. В равной степени я предпочитаю избегать nothing потому что это часто связано с null что, в принципе, это то, что null Однако следует придерживаться этого принципа по любым практическим причинам.

Что именно является исключением?

Для любого опытного программиста это тривиальный вопрос, но я бы хотел сказать несколько слов, чтобы погасить червя путаницы:

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

Это может варьироваться от отбрасывания любых деталей и возврата единственного глобального значения (например, NaN или же null) или создать длинный список журналов или что именно произошло, отправить его в базу данных и реплицировать по всему распределенному слою хранения данных;)

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

Разрешены ли исключения в чистых функциях?

Краткий ответ: да, но только тогда, когда они не приводят к побочным эффектам.

Более длинный ответ. Чтобы быть чистым, вывод вашей функции должен быть однозначно определен ее вводом. Поэтому мы изменим нашу функцию f отправив 0 к новому абстрактному значению e что мы называем исключением. Мы уверены, что значение e не содержит никакой внешней информации, которая однозначно не определяется нашими данными, x, Итак, вот пример исключения без побочных эффектов:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

И вот один с побочным эффектом:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

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

Чтобы сделать это еще глупее. Функция, возвращающая 42 всегда чисто Но если кто-то сумасшедший решит сделать 42 переменная, значение которой может измениться, та же самая функция перестает быть чистой в новых условиях.

Обратите внимание, что я использую буквенное обозначение объекта для простоты, чтобы продемонстрировать суть. К сожалению, вещи перепутаны в таких языках, как JavaScript, где error это не тип, который ведет себя так, как мы хотим здесь, относительно композиции функций, в то время как фактические типы, такие как null или же NaN не ведите себя так, а скорее проходите через некоторые искусственные и не всегда интуитивные преобразования типов.

Тип расширения

Поскольку мы хотим изменить сообщение внутри нашего исключения, мы действительно объявляем новый тип E для всего объекта исключения, а затем это то, что maybe number кроме своего запутанного имени, которое должно быть любого типа number или нового типа исключения E так что это действительно союз number | E из number а также E, В частности, это зависит от того, как мы хотим построить E, который не предлагается и не отражен в названии maybe number,

Что такое функциональная композиция?

Это математическая операция, принимающая функции f: X -> Y а также g: Y -> Z и построение их композиции как функции h: X -> Z сытный h(x) = g(f(x)), Проблема с этим определением возникает, когда результат f(x) не допускается в качестве аргумента g,

В математике эти функции не могут быть составлены без дополнительной работы. Строго математическое решение для нашего приведенного выше примера f а также g это удалить 0 из набора определения f, С этим новым набором определений (новый более ограничительный тип x), f становится составным с g,

Однако в программировании не очень практично ограничивать набор определений f как это. Вместо этого могут быть использованы исключения.

Или как другой подход, искусственные ценности создаются как NaN, undefined, null, Infinity и т.д. Так вы оцениваете 1/0 в Infinity а также 1/-0 в -Infinity, И затем принудительно верните новое значение в ваше выражение, вместо того, чтобы вызывать исключение. Приводя к результатам вы можете или не можете найти предсказуемые:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

И мы вернулись к обычным номерам, готовым двигаться дальше;)

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

Но является ли правило составления функции, возникающей из реализации JavaScript для обработки числовых ошибок, монадой?

Чтобы ответить на этот вопрос, все, что вам нужно, это проверить аксиомы (оставленное здесь как упражнение, не являющееся частью вопроса;).

Можно ли использовать исключение для создания монады?

На самом деле, более полезной монадой было бы правило, предписывающее, что если f исключение для некоторых x так же как и его состав с любым g, Плюс сделай исключение E глобально уникальное с единственно возможным значением ( конечный объект в теории категорий). Теперь две аксиомы мгновенно проверяются, и мы получаем очень полезную монаду. И результат - то, что известно как возможно монада.

У вас есть недавняя презентация Кристофера Лиги " Монадология - профессиональная помощь по типу тревоги " (12 июля 2010 г.), которая довольно интересна по темам продолжения и монады.
Видео с этой (слайд-шоу) презентацией действительно доступно на vimeo.
Партия монады начинается примерно через 37 минут в этом часовом видео и начинается со слайда 42 из его 58 презентаций слайдов.

Он представлен как "ведущий шаблон проектирования для функционального программирования", но в примерах используется язык Scala, который является одновременно ООП и функциональным.
Вы можете прочитать больше о Monad в Scala в посте блога " Monads - еще один способ абстрагировать вычисления в Scala ", от Debasish Ghosh (27 марта 2008 г.).

Конструктор типа M является монадой, если он поддерживает следующие операции:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Так, например (в Scala):

  • Option это монада
    единица измерения [A] (x: A): опция [A] = некоторая (x)

    def flatMap [A, B] (m: опция [A])(f:A => опция [B]): опция [B] =
      m match {
       дело нет => нет
       case Some(x) => f(x)
      }
  • List это монада
    единица измерения [A] (x: A): список [A] = список (x)

    def flatMap [A, B] (m: список [A])(f:A => список [B]): список [B] =
      m match {
        дело ноль => ноль
        case x:: xs => f (x)::: flatMap (xs) (f)
      }

Monad имеет большое значение в Scala из-за удобного синтаксиса, созданного для использования преимуществ структур Monad:

for понимание в Scala:

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

переводится компилятором в:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

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

В приведенном выше фрагменте flatMap принимает в качестве входных данных замыкание (SomeType) => List[AnotherType] и возвращает List[AnotherType], Важно отметить, что все flatMaps принимают один и тот же тип закрытия как входные данные и возвращают тот же тип, что и выходные данные.

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


Если вы берете две операции (которые могут потерпеть неудачу) и передаете результат третьей, например:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

но не используя Monad, вы получаете запутанный ООП-код, такой как:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

тогда как с Monad вы можете работать с реальными типами (Venue, User), как все операции работают, и держите материал проверки Option скрытым, все из-за плоских карт синтаксиса for:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

Часть yield будет выполнена, только если все три функции имеют Some[X]; любой None будет непосредственно возвращен confirm,


Так:

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

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

Это упорядочение и создание потоков действий монадой осуществляется языковым компилятором, который выполняет преобразование посредством магии замыканий.


Кстати, Monad - это не только модель вычислений, используемая в FP: см. Этот пост в блоге.

Теория категорий предлагает множество моделей вычислений. Среди них

  • стрелочная модель вычислений
  • модель вычислений Monad
  • Аппликативная модель расчетов

Монада - это тип данных, который инкапсулирует значение и к которому, по сути, можно применить две операции:

  • return x создает значение типа монады, которое инкапсулирует x
  • m >>= f (читается как "оператор связывания") применяет функцию f к значению в монаде m

Вот что такое монада. Есть еще несколько технических особенностей, но в основном эти две операции определяют монаду. Реальный вопрос заключается в том, "Что делает монада?", И это зависит от монада - списки - это монады, Maybes - это монады, операции ввода-вывода - это монады. Все, что это означает, когда мы говорим, что это монады, это то, что они имеют интерфейс монады return а также >>=,

Я написал небольшую статью, в которой сравнивался стандартный код OOP Python с монадическим кодом Python, демонстрирующий базовый вычислительный процесс с помощью диаграмм. Это не предполагает никаких предварительных знаний о ФП. Надеюсь, вы найдете это полезным - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/

Из википедии:

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

Формально монада создается путем определения двух операций (связывание и возврат) и конструктора типа M, который должен выполнять несколько свойств, чтобы обеспечить правильную композицию монадических функций (то есть функций, которые используют значения из монады в качестве аргументов). Операция return берет значение из простого типа и помещает его в монадический контейнер типа M. Операция связывания выполняет обратный процесс, извлекая исходное значение из контейнера и передавая его в соответствующую следующую функцию в конвейере.

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

Я считаю, что это очень хорошо объясняет.

Я постараюсь сделать кратчайшее определение, которое я могу использовать, используя термины ООП:

Общий класс CMonadic<T> является монадой, если она определяет по крайней мере следующие методы:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

и если следующие законы применяются для всех типов T и их возможных значений t

левая личность:

CMonadic<T>.create(t).flatMap(f) == f(t)

правильная идентичность

instance.flatMap(CMonadic<T>.create) == instance

ассоциативность:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Примеры:

Монада List может иметь:

List<int>.create(1) --> [1]

И flatMap в списке [1,2,3] может работать так:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Итерируемые и наблюдаемые также могут быть сделаны монадическими, а также обещаниями и заданиями.

Комментарий:

Монады не так сложны. flatMap функция очень похожа на более часто встречающиеся map, Он получает аргумент функции (также известный как делегат), который он может вызывать (немедленно или позже, ноль или более раз) со значением, полученным из универсального класса. Ожидается, что переданная функция также обернет свое возвращаемое значение в тот же тип универсального класса. Чтобы помочь с этим, он обеспечивает createконструктор, который может создать экземпляр этого универсального класса из значения. Возвращаемый результат flatMap также является универсальным классом того же типа, часто упаковывая те же значения, которые содержались в результатах возврата одного или нескольких приложений flatMap, в ранее содержащиеся значения. Это позволяет вам связывать flatMap столько, сколько вы хотите:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

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

Например, вы можете моделировать исключения, используя монадические контейнеры. Каждый контейнер будет содержать либо результат операции, либо произошедшую ошибку. Следующая функция (делегат) в цепочке обратных вызовов flatMap будет вызываться, только если предыдущая упаковала значение в контейнер. В противном случае, если ошибка была запакована, она будет продолжать распространяться по цепочечным контейнерам до тех пор, пока не будет найден контейнер, к которому прикреплена функция обработчика ошибок с помощью метода, называемого .orElse() (такой метод будет допустимым расширением)

Примечания: Функциональные языки позволяют вам писать функции, которые могут работать с любым типом монадического универсального класса. Чтобы это работало, нужно написать общий интерфейс для монад. Я не знаю, возможно ли написать такой интерфейс на C#, но насколько я знаю, это не так:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}

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

Другими словами, монада - это «повторитель функций» или «цепочка функций», которые связывают и оценивают значения аргументов в автоматически поддерживаемой среде. Часто связанные значения аргументов являются функциями обновления, но на самом деле могут быть любыми объектами. Монада - это «связующий код», выполняемый перед каждым вычисляемым аргументом. Этот связующий код должен интегрировать вывод каждого аргумента в среду. Таким образом, монада объединяет результаты всех вызовов обновления (или всех преобразований элементов контейнера) способом, зависящим от реализации конкретной монады.

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

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

Монады для ООП людей

В ООП монада - это типичный объект с

  • конструктор часто называют который превращает значение в начальный экземпляр среды

  • часто вызываемый связанный в цепочку метод приложения обновления, который поддерживает состояние объекта с выходом входной "функции обновления"

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

Монада может реализовывать паттерн Строитель, но допускает гораздо более широкое использование.

Пример (Python)

Я думаю, что наиболее интуитивно понятным примером монад являются реляционные операторы из Python:

      result =  0 <= x == y < 3

Вы видите, что это монада, потому что она должна нести некоторое логическое состояние, которое не известно отдельным вызовам реляционных операторов.

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

      # result = ret(0)
result = (0, true)
# result = result.bind(lambda v: (x, v <= x))
result[1] = result[1] and result[0] <= x
result[0] = x
# result = result.bind(lambda v: (y, v == y))
result[1] = result[1] and result[0] == y
result[0] = y
# result = result.bind(lambda v: (3, v < 3))
result[1] = result[1] and result[0] < 3
result[0] = 3
result = result[1]      # not explicit part of a monad

Настоящая монада вычислит каждый аргумент не более одного раза.

Теперь подумайте о переменной "результат", и вы получите такую ​​цепочку:

      ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)

Имеет ли монада "естественную" интерпретацию в ОО, зависит от монады. В таком языке, как Java, вы можете перевести возможную монаду в язык проверки на наличие нулевых указателей, чтобы неудачные вычисления (т. Е. Ничего не производящие в Haskell) испускали нулевые указатели в качестве результатов. Вы можете перевести монаду состояния на язык, созданный путем создания изменяемой переменной и методов для изменения ее состояния.

Монада - это моноид в категории эндофункторов.

Информация, которую складывает предложение, очень глубокая. И вы работаете в монаде с любым императивным языком. Монада - это "упорядоченный" предметно-ориентированный язык. Он удовлетворяет определенным интересным свойствам, которые в совокупности делают монаду математической моделью "императивного программирования". Haskell позволяет легко определять маленькие (или большие) императивные языки, которые можно комбинировать различными способами.

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

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

Для этой цели можно использовать монадные трансформаторы, и существует коллекция высокого качества всех "стандартных" монад:

  • Списки (недетерминированные вычисления, рассматривая список как домен)
  • Возможно (вычисления, которые могут быть неудачными, но для которых отчетность не важна)
  • Ошибка (вычисления, которые могут дать сбой и требуют обработки исключений
  • Reader (вычисления, которые могут быть представлены композициями простых функций Haskell)
  • Writer (вычисления с последовательным "рендерингом"/"ведением журнала" (в строки, html и т. Д.)
  • Продолжение (продолжение)
  • IO (вычисления, которые зависят от базовой компьютерной системы)
  • Состояние (вычисления, контекст которых содержит изменяемое значение)

с соответствующими монадными трансформаторами и типами классов. Классы типов допускают дополнительный подход к объединению монад путем объединения их интерфейсов, так что конкретные монады могут реализовать стандартный интерфейс для "вида" монады. Например, модуль Control.Monad.State содержит класс MonadState s m, а (State s) является экземпляром формы

instance MonadState s (State s) where
    put = ...
    get = ...

Длинная история состоит в том, что монада - это функтор, который присоединяет "контекст" к значению, который имеет способ ввести значение в монаду, и который имеет способ оценивать значения по отношению к контексту, связанному с ним, по крайней мере ограниченным образом.

Так:

return :: a -> m a

это функция, которая вводит значение типа a в монадное "действие" типа m a.

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

это функция, которая выполняет действие монады, оценивает его результат и применяет функцию к результату. Особенность (>>=) в том, что результат находится в той же монаде. Другими словами, в m >>= f (>>=) извлекает результат из m и связывает его с f, так что результат находится в монаде. (В качестве альтернативы мы можем сказать, что (>>=) тянет f к m и применяет его к результату.) Как следствие, если у нас есть f:: a -> m b и g:: b -> m c, мы можем "последовательность" действий:

m >>= f >>= g

Или, используя "сделать нотацию"

do x <- m
   y <- f x
   g y

Тип для (>>) может быть светящимся. это

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

Это соответствует оператору (;) в процедурных языках, таких как C. Это позволяет делать обозначения, такие как:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

В математической и философской логике у нас есть фреймы и модели, которые "естественно" моделируются с помощью монадизма. Интерпретация - это функция, которая изучает область модели и вычисляет истинное значение (или обобщения) предложения (или формулы при обобщениях). В модальной логике необходимости мы можем сказать, что суждение необходимо, если оно истинно в "каждом возможном мире" - если оно верно в отношении любой допустимой области. Это означает, что модель на языке для предложения может быть преобразована в модель, область которой состоит из набора различных моделей (одна, соответствующая каждому возможному миру). Каждая монада имеет метод с именем "join", который выравнивает слои, что подразумевает, что каждое монадное действие, результатом которого является монадное действие, может быть встроено в монаду.

join :: m (m a) -> m a

Что еще более важно, это означает, что монада закрыта в соответствии с операцией "укладки слоев". Вот как работают монадные преобразователи: они объединяют монады, предоставляя "похожие" методы для таких типов, как

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

так что мы можем преобразовать действие в (MaybeT m) в действие в m, эффективно сворачивая слои. В этом случае runMaybeT:: MaybeT m a -> m (может быть, a) является нашим методом, подобным объединению. (MaybeT m) является монадой, а MaybeT:: m (Может быть, a) -> MaybeT m a фактически является конструктором для нового типа действия монады в m.

Свободная монада для функтора - это монада, генерируемая суммированием f, из которой следует, что каждая последовательность конструкторов для f является элементом свободной монады (или, точнее, той же самой формы, что и дерево последовательностей конструкторов для е). Свободные монады являются полезным методом для построения гибких монад с минимальным количеством котельной плиты. В программе на Haskell я мог бы использовать свободные монады для определения простых монад для "системного программирования высокого уровня", чтобы помочь поддерживать безопасность типов (я просто использую типы и их объявления. Реализации просты с использованием комбинаторов):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Монадизм - это основополагающая архитектура для того, что вы могли бы назвать шаблоном "интерпретатор" или "команда", абстрагируясь до его наиболее четкой формы, поскольку каждое монадическое вычисление должно быть "выполнено", по крайней мере, тривиально. (Система времени выполнения запускает для нас монаду ввода-вывода и является точкой входа в любую программу на Haskell. IO "управляет" остальными вычислениями, выполняя действия IO по порядку).

Тип для соединения также является тем, где мы получаем утверждение, что монада является моноидом в категории эндофункторов. Присоединение, как правило, более важно для теоретических целей, в силу его типа. Но понимание типа означает понимание монад. Соединяющие и монадные типы, сходные с трансформатором, по сути являются композициями эндофункторов в смысле композиции функций. Чтобы поместить это в псевдо-язык, похожий на Haskell,

Foo:: m (ma) <-> (m. M) a

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

С точки зрения ОО, монада - это свободный контейнер.

Минимальным требованием является определение class <A> Something который поддерживает конструктор Something(A a) и хотя бы один метод Something<B> flatMap(Function<A, Something<B>>)

Возможно, он также считает, есть ли в вашем классе монады какие-либо методы с сигнатурой Something<B> work() который сохраняет правила класса - компилятор запекает в flatMap во время компиляции.

Почему монада полезна? Потому что это контейнер, который позволяет цепочечные операции, которые сохраняют семантику. Например, Optional<?> сохраняет семантику isPresent для Optional<String>, Optional<Integer>, Optional<MyClass>, так далее.

В качестве грубого примера,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Обратите внимание, что мы начинаем со строки и заканчиваем целым числом. Довольно круто.

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

Таким образом вы сохраняете семантику - т.е. значение и операции контейнера не меняются, они просто обертывают и улучшают объект внутри контейнера.

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

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

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

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

Поток управления сохраняется, но неожиданное событие безопасно инкапсулируется и обрабатывается.

Монада - это массив функций

(Pst: массив функций - это просто вычисление).

На самом деле, вместо истинного массива (одна функция в одном массиве ячеек) у вас есть эти функции, связанные другой функцией >>=. >> = позволяет адаптировать результаты функции i к функции подачи i+1, выполнять вычисления между ними или даже не вызывать функцию i+1.

Используемые здесь типы - это "типы с контекстом". Это значение с тегом. Цепные функции должны принимать "голое значение" и возвращать тегированный результат. Одной из обязанностей >> = является извлечение обнаженного значения из его контекста. Также есть функция return, которая принимает голое значение и помещает его с тегом.

Пример с Возможно. Давайте использовать его для хранения простого целого числа, по которому производим вычисления.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

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

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

И это будет использоваться так:

print (runMyMonad (Just 160) myArray1)

Самое простое объяснение, которое я могу придумать, - это то, что монады - это способ составления функций с приукрашенными результатами (также известный как композиция Клейсли). "Украшенная" функция имеет подписьa -> (b, smth) где a а также b типы (думаю Int, Bool), которые могут отличаться друг от друга, но не обязательно - и smth это "контекст" или "украшение".

Этот тип функций также можно записать a -> m b где m эквивалентно "украшению" smth. Итак, это функции, которые возвращают значения в контексте (подумайте о функциях, которые регистрируют свои действия, гдеsmthэто сообщение журнала; или функции, которые выполняют ввод \ вывод и их результаты зависят от результата действия ввода / вывода).

Монада - это интерфейс ("класс типов"), который заставляет разработчика указывать, как составлять такие функции. Разработчик должен определить композиционную функцию(a -> m b) -> (b -> m c) -> (a -> m c) для любого типа m который хочет реализовать интерфейс (это композиция Клейсли).

Итак, если мы говорим, что у нас есть тип кортежа (Int, String) представляющие результаты вычислений на Ints, которые также регистрируют свои действия, с (_, String) "украшение" - журнал действий - и две функции increment :: Int -> (Int, String) а также twoTimes :: Int -> (Int, String) мы хотим получить функцию incrementThenDouble :: Int -> (Int, String) который представляет собой композицию двух функций, которая также учитывает журналы.

В данном примере монадная реализация двух функций применяется к целочисленному значению 2 incrementThenDouble 2 (что равно twoTimes (increment 2)) вернется (6, " Adding 1. Doubling 3.") для промежуточных результатов increment 2 равно (3, " Adding 1.") а также twoTimes 3 равно (6, " Doubling 3.")

Из этой композиционной функции Клейсли можно вывести обычные монадические функции.

Простое объяснение Monads с примером использования Marvel здесь.

Монады - это абстракции, используемые для последовательного выполнения зависимых функций. Эффект здесь означает, что они возвращают тип в форме F [A], например Option[A], где Option - это F, называемый конструктором типов. Давайте посмотрим на это в 2 простых шага

  1. Ниже функция композиции является переходной. Таким образом, чтобы перейти от A к C I, можно составить A => B и B => C.
 A => C   =   A => B  andThen  B => C

  1. Однако, если функция возвращает тип эффекта, такой как Option[A], то есть A => F[B], композиция не работает, так как для перехода к B нам нужно A => B, но у нас есть A => F[B].

    Нам нужен специальный оператор "bind", который знает, как объединить эти функции, которые возвращают F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

Функция "связать" определена для конкретного F.

Существует также "return" типа A => F [A] для любого A, определенного для этого конкретного F. Чтобы быть Монадой, F должен иметь эти две функции, определенные для нее.

Таким образом, мы можем построить эффективную функцию A => F[B] из любой чистой функции A => B,

 A => F[B]   =   A => B  andThen  return

но данный F также может определять свои собственные непрозрачные "встроенные" специальные функции таких типов, которые пользователь не может определить сам (на чистом языке), например

  • "Случайный" (Range => Random [Int])
  • "печать" (String => IO [()])
  • "попробуй... поймай" и т. д.

С практической точки зрения (суммируя то, что было сказано во многих предыдущих ответах и ​​связанных статьях), мне кажется, что одна из фундаментальных "целей" (или полезности) монады заключается в использовании зависимостей, неявных в рекурсивных вызовах методов. aka композиция функций (т. е. когда f1 вызывает f2, вызывает f3, f3 необходимо оценить перед f2 перед f1), чтобы представить последовательную композицию естественным образом, особенно в контексте ленивой модели оценки (то есть последовательная композиция в виде простой последовательности Например, "f3(); f2(); f1();" в C - трюк особенно очевиден, если вы подумаете о случае, когда f3, f2 и f1 фактически ничего не возвращают [их цепочка как f1(f2(f3)) является искусственным, чисто предназначенным для создания последовательности]).

Это особенно актуально, когда вовлечены побочные эффекты, то есть когда какое-то состояние изменяется (если бы у f1, f2, f3 не было побочных эффектов, не было бы никакого значения, в каком порядке они оцениваются; это большое свойство чистого функциональные языки, чтобы иметь возможность распараллелить эти вычисления, например). Чем больше чистых функций, тем лучше.

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

Это только один аспект, как и здесь.

Если вы когда-либо использовали Powershell, описанные Эриком шаблоны должны звучать знакомо. Командлеты Powershell - это монады; Функциональная композиция представлена конвейером.

Интервью Джеффри Сновера с Эриком Мейером становится более подробным.

Смотрите мой ответ "Что такое монада?"

Он начинается с мотивирующего примера, проходит через пример, выводит пример монады и формально определяет "монаду".

Он не предполагает никаких знаний о функциональном программировании и использует псевдокод с function(argument) := expression синтаксис с простейшими возможными выражениями.

Эта программа на C++ является реализацией монады псевдокода. (Для справки: M это конструктор типа, feed это операция "связать", и wrap это операция "возврата".)

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

/ maybeявляется наиболее фундаментальным монадическим типом

Монады связаны с функциональной композицией. Если у вас есть функции f:optional<A>->optional<B>, g:optional<B>->optional<C>, h:optional<C>->optional<D>. Тогда вы могли бы составить их

      optional<A> opt;
h(g(f(opt)));

Преимущество монадных типов в том, что вместо этого вы можете составлять f:A->optional<B>, g:B->optional<C>, h:C->optional<D>. Они могут это сделать, потому что монадический интерфейс предоставляет оператор связывания

      auto optional<A>::bind(A->optional<B>)->optional<B>

и композиция могла быть написана

      optional<A> opt
opt.bind(f)
   .bind(g)
   .bind(h)

Преимущество монад в том, что нам больше не нужно обрабатывать логику if(!opt) return nullopt;в каждом из f,g,hпотому что эта логика перенесена в оператор привязки.

/ lists/ iterablesявляются вторым наиболее фундаментальным типом монад.

Монадическая особенность rangesмы можем преобразовать, а затем сгладить, т.е. начиная с предложения, закодированного как диапазон целых чисел [36, 98]

мы можем превратиться в [['m','a','c','h','i','n','e',' '], ['l','e','a','r','n','i','n','g', '.']]

а потом сгладить ['m','a','c','h','i','n','e', ' ', 'l','e','a','r','n','i','n','g','.']

Вместо того, чтобы писать этот код

      vector<string> lookup_table;
auto stringify(vector<unsigned> rng) -> vector<char>
{
    vector<char> result;
    for(unsigned key : rng)
       for(char ch : lookup_table[key])
           result.push_back(ch);
       result.push_back(' ')
    result.push_back('.')
    return result
}

мы могли бы написать написать это

      auto f(unsigned key) -> vector<char>
{
    vector<char> result;
    for(ch : lookup_table[key])
        result.push_back(ch);
    return result
}
auto stringify(vector<unsigned> rng) -> vector<char>
{
    return rng.bind(f);
}

Монада запускает цикл for for(unsigned key : rng)в функцию привязки, что позволяет использовать код, о котором теоретически легче рассуждать. Пифагорейские тройки могут быть сгенерированы в range-v3 с помощью вложенных привязок (а не связанных привязок, как мы видели с optional)

      auto triples =
  for_each(ints(1), [](int z) {
    return for_each(ints(1, z), [=](int x) {
      return for_each(ints(x, z), [=](int y) {
        return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z));
      });
    });
  });

С точки зрения того, что программист ООП поймет (без какой-либо функциональной основы программирования), что такое монада?

Нет такого объяснения, о котором я знаю, и предположения о том, что он существует, противоречит высокомерию программистов ООП.

Какую проблему он решает и какие места он использует чаще всего?

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

Чтобы прояснить то понимание, которое я искал, допустим, вы конвертировали приложение FP с монадами в приложение ООП. Что бы вы сделали, чтобы перенести обязанности монад в приложение ООП?

Поскольку лень редко встречается в ООП, то по большей части они не имеют значения. Однако вы можете перенести монадическую обработку ошибок.

Другие вопросы по тегам