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

743

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

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

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

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


источник
10
Этот пост очень хорош: blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
Паскаль Куок
10
@Pavel: Ответ, который мы получили ниже от Эрика, намного лучше, чем ответы в тех других предложенных Q для людей с фоном OO (в отличие от фона FP).
Donal Fellows
5
@Donal: Если это простофиля (о которой я не имею никакого мнения), хороший ответ должен быть добавлен к оригиналу. То есть: хороший ответ не исключает закрытия как дубликата. Если это достаточно близкий дубликат, это может быть выполнено модератором в виде слияния.
dmckee --- котенок экс-модератора
3
Смотрите также: stackoverflow.com/questions/674855/...
н

Ответы:

732

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

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

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

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

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

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

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

тогда соответствующая функция on 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собственностью.

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

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

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>может теперь применять к нему семантику, допускающую значение NULL .

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

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 # я настоятельно рекомендую мою ( Эрика Липперта ) статью коллеги Уэса Дайера на эту тему. Эта статья - то, что объяснило мне монады, когда они наконец "щелкнули" для меня.
  2. Хорошая иллюстрация того, почему вы можете захотеть монаду (использует Haskell в своих примерах) .
  3. Вроде «перевод» предыдущей статьи на JavaScript.

Eric Lippert
источник
17
Это отличный ответ, но у меня голова разболелась. Я буду следить за этим и смотреть на него в эти выходные и задавать вам вопросы, если что-то не успокоится и не будет иметь смысла в моей голове.
Пол Натан
5
Отличное объяснение, как обычно, Эрик. Для более теоретического (но все еще очень интересного) обсуждения я нашел сообщение в блоге Барта де Смета о MinLINQ, помогающее связать некоторые конструкции функционального программирования с C # также. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Рон Уорхолик
41
Для меня имеет больше смысла говорить, что это увеличивает типы, а не усиливает их.
Гейб
61
@slomojo: и я изменил его обратно на то, что я написал и намеревался написать. Если вы с Гейбом хотите написать свой собственный ответ, вы идете прямо вперед.
Эрик Липперт
24
@Eric, до вас конечно, но Amplifier подразумевает, что существующие свойства усиливаются, что вводит в заблуждение.
ocodo 19.10.10
341

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

  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чтобы использовать a Maybe Real.

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

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

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

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

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

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

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

cibercitizen1
источник
2
@DmitriZaitsev Исключения могут происходить только в "нечистом коде" (монаде IO), насколько я знаю.
cibercitizen1
3
@DmitriZaitsev Роль Ничто может играть любой другой тип (отличный от ожидаемого Реального). Дело не в этом. В этом примере вопрос заключается в том, как адаптировать функции в цепочке, когда предыдущий может возвращать неожиданный тип значения следующему, не цепляя последний (принимая только Real как входное значение).
cibercitizen1
3
Еще одна путаница заключается в том, что слово «монада» встречается в вашем ответе только два раза и только в сочетании с другими терминами - Stateи IOни с одним из них, а также с точным значением слова «монада»
Дмитрий Зайцев
31
Для меня, как человека, происходящего из ООП, этот ответ действительно хорошо объяснил мотивацию наличия монады, а также то, чем на самом деле является монада (гораздо больше, чем принятый ответ). Так что я нахожу это очень полезным. Большое спасибо @ cibercitizen1 и +1
ахилесс
3
Я уже почти год читаю о функциональном программировании. Этот ответ, и особенно первые два пункта, наконец-то позволили мне понять, что на самом деле означает императивное программирование и почему функциональное программирование отличается. Спасибо!
Джрахали
82

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

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

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

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

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

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

JacquesB
источник
15
Я полагаю, что это первое монадическое объяснение, которое я видел, которое не опиралось на концепции функционального программирования и выражало его в реальных терминах ООП. Действительно хороший ответ.
Дэвид К. Хесс
это очень близко 2 к тому, что монады на самом деле есть в FP / Haskell, за исключением того, что сами объекты команд «знают», к какой «логике вызова» они принадлежат (и только совместимые могут быть связаны вместе); invoker просто предоставляет первое значение. Это не значит, что команда «Печать» может быть выполнена «недетерминированной логикой выполнения». Нет, это должна быть «логика ввода / вывода» (т. Е. Монада ввода / вывода). Но кроме этого, это очень близко. Можно даже сказать, что монады - это просто программы (построенные из операторов кода, которые будут выполнены позже). В первые дни о «связывании» говорили как о «программируемой точке с запятой» .
Уилл Несс
1
@ DavidK.Hess Я действительно скептически отношусь к ответам, использующим FP для объяснения основных понятий FP, и особенно к ответам, использующим язык FP, такой как Scala. Молодец, Жак!
Восстановить Монику
62

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

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

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

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

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

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

BMeph
источник
1
returnна самом деле не будет методом для монады, потому что он не принимает экземпляр монады в качестве аргумента. (т.е.: это не / я)
Лоуренс Гонсалвес
@LaurenceGonsalves: Поскольку в настоящее время я рассматриваю это для своей дипломной работы бакалавра, я думаю, что главным ограничением является отсутствие статических методов в интерфейсах в C # / Java. Вы могли бы далеко продвинуться в направлении реализации всей истории монады, по крайней мере, статически связанной, а не основанной на классах типов. Интересно, что это сработало бы даже несмотря на отсутствие более добрых типов.
Себастьян Граф
42

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

Он представлен как «ведущий шаблон проектирования для функционального программирования», но в примерах используется язык Scala, который является одновременно ООП и функциональным.
Вы можете прочитать больше о Monad в Scala в посте блога « Monads - еще один способ абстрагировать вычисления в Scala », от Дебасиша Гоша (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]и возвращает a 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
  • Аппликативная модель расчетов
VonC
источник
2
Я люблю это объяснение! Пример, который вы привели, прекрасно демонстрирует концепцию, а также добавляет то, чего ИМХО не хватало в тизере Эрика о том, что SelectMany () является Монадой. Спасибо за это!
Aoven
1
ИМХО это самый элегантный ответ
Полимераза
и прежде всего, Функтор.
Уилл Несс
34

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

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

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

  • отправляет каждый тип 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(изнутри).
  • Унитальное свойство : fсоставление с функцией идентичности с обеих сторон должно дать f.

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

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

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

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

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


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


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

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

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

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глобально уникальным только с одним возможным значением ( конечный объект в теории категорий). Теперь две аксиомы мгновенно проверяются, и мы получаем очень полезную монаду. И результат - то, что известно как возможно монада .

Дмитрий Зайцев
источник
3
Хороший вклад. +1 Но, может быть, вы захотите удалить фразу «нашел слишком много объяснений слишком долго ...», так как она самая длинная. Другие будут судить, является ли это «простым английским» по мере необходимости, вопросом: «простой английский == простыми словами, простым способом».
cibercitizen1
@ cibercitizen1 Спасибо! Это на самом деле короткий, если не считать пример. Суть в том, что вам не нужно читать пример, чтобы понять определение . К сожалению, многие объяснения заставляют меня сначала читать примеры , что часто не нужно, но, конечно, может потребоваться дополнительная работа для автора. При слишком большой зависимости от конкретных примеров существует опасность того, что незначительные детали затеняют картину и затруднят ее понимание. Сказав это, у вас есть действительные баллы, смотрите обновление.
Дмитрий Зайцев
2
слишком долго и сбивает с толку
seenimurugan
1
@seenimurugan Предложения по улучшению приветствуются;)
Дмитрий Зайцев
26

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

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

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

цыпленок
источник
«Что делает монада, и это зависит от монады», а точнее, это зависит от bindфункции, которая должна быть определена для каждого монадического типа, не так ли? Это было бы хорошей причиной, чтобы не путать связывание с композицией, поскольку существует единственное определение для композиции, хотя для функции связывания не может быть только одного определения, для монадического типа - по одному, если я правильно понимаю.
Hibou57
14

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

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

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

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

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

the_drow
источник
12

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

Универсальный класс 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
}
Горги Косев
источник
7

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

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

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

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

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

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

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

с соответствующими монадными трансформаторами и типами классов. Классы типов допускают дополнительный подход к объединению монад, объединяя их интерфейсы, так что конкретные монады могут реализовать стандартный интерфейс для «вида» монады. Например, модуль Control.Monad.State содержит класс MonadState sm, а (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 -> mb и g :: b -> mc, мы можем «последовательность» действий:

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 ma -> m (может быть, a) - это наш метод соединения. (MaybeT m) является монадой, а MaybeT :: m (Maybe a) -> MaybeT ma фактически является конструктором для нового типа действия монады в 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

без мужчин
источник
3

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

(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)
cibercitizen1
источник
1
Супер-аккуратный! Так что bind - это просто способ оценить массив функций с контекстом, последовательно, на входе с контекстом :)
Муса Аль-Хасси,
>>=это оператор
user2418306
1
Я думаю, что аналогия с «массивом функций» мало проясняет. Если \x -> x >>= k >>= l >>= mэто массив функций, то есть h . g . f, который не включает в себя монады вообще.
дублировать
мы могли бы сказать, что функторы , будь то монадические, аппликативные или простые, - это «украшенное приложение» . 'Applicative' добавляет цепочку, а 'monad' добавляет зависимость (т.е. создает следующий шаг вычисления в зависимости от результатов предыдущего шага вычисления).
Уилл Несс
3

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

Минимальным требованием является определение того, 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)

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

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

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

обкрадывать
источник
2

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

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

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

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

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

Дэвид К. Хесс
источник
2

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

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

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

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

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

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

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

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

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

Gulshan
источник
1

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

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

Он не предполагает никаких знаний о функциональном программировании и использует псевдокод с 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;
}
Иордания
источник
0

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

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

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

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

Novis
источник
0

Самое простое объяснение, которое я могу придумать, заключается в том, что монады - это способ составления функций с впечатляющими результатами (он же состав Клейсли). «Украшенная» функция имеет сигнатуру 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.")

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

RedPoppy
источник