it-swarm.com.ru

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

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

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

EDIT:

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

657
user65663

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

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

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

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

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

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

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

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

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

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

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

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

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

Снова, возьмите 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 и возвращает Nullable<int>, потому что вы можете просто запустить результат через конструктор, допускающий нулевое значение. Теперь предположим, что у вас есть метод высшего порядка:

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 с монадами в приложение OOP. Что бы вы сделали, чтобы перенести обязанности монад в приложение OOP?

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

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


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

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

674
Eric Lippert

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

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

    f(x) = 2 * x

    g(x,y) = x / y

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

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

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

    Решение: Давайте разрешить функциям возвращать два вида вещей: вместо того, чтобы иметь 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 monad.

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

276
cibercitizen1

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

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

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

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

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

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

68
JacquesB

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

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

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

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

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

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

60
BMeph

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

Он представлен как "ведущий шаблон проектирования для функционального программирования", но в примерах используется язык Scala, который является одновременно [OOP] и функциональным.
Вы можете прочитать больше о 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является монадой
 def единица [A] (x: A): опция [A] = некоторая (x) 
 
 def flatMap [A, B] (m: опция [A]) (f: A => Опция [B]): Опция [B] = 
 m соответствует {
 case Нет => Нет 
 case Some (x) => f (x ) 
} 
  • Listis Monad
 def unit [A] (x: A): список [A] = список (x) 
 
 def flatMap [A, B] (m: список [A]) (f: A => Список [B]): Список [B] = 
 m соответствует {
 case Nil => Nil 
 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 }}}

Ключевая абстракция - это flatMapname__, которая связывает вычисления посредством цепочки.
Каждый вызов 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 вы можете работать с реальными типами (Venuename__, Username__), как и все операции, и сохранять скрытые элементы проверки Option, все из-за плоских карт синтаксиса for:

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

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


Так:

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

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

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


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

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

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

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

34
Nikolay

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

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

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

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

26
Dmitri Zaitsev

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

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

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

25
Chuck

От Википедия :

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

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

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

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

11
the_drow

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

Универсальный класс 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
}
11
Gorgi Kosev

То, имеет ли монада "естественное" истолкование в OO, зависит от монады. В таком языке, как 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 -> 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 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))

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

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

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

7
nomen

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

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

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

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

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

-- 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)
3
cibercitizen1

Простое объяснение Монад с примером из практики Marvel: здесь .

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

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

enter image description here

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

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

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

Функция "bind" определена для конкретной F.

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

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

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

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

  • "random" ( Range => Random [Int] )
  • "print" ( String => IO [()] )
  • "попробуй ... поймай" и т. д.
2
Ira

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

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

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

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

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

2
David K. Hess

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

Минимальное требование - это определение 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, соответствует критерию функции контейнера, которая возвращает контейнер исходного типа.

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

1
Rob

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

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

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

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

#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;
}
1
Jordan

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

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

1
Richard Berg

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

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

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

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

0
novis