Что такое монада?


1416

Кратко рассмотрев недавно Хаскелла, каким было бы краткое, сжатое, практическое объяснение того, что в действительности представляет собой монада?

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


12
Эрик Липперт написал ответ на этот вопрос ( stackoverflow.com/questions/2704652/… ), который из-за некоторых проблем находится на отдельной странице.
П Швед

70
Вот новое введение с использованием JavaScript - я нашел его очень читабельным.
Бенджол


20
Смотрите также Монады в картинках
cibercitizen1

2
Монада - это массив функций с вспомогательными операциями. Смотрите этот ответ
cibercitizen1

Ответы:


1060

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

Вы просите практических примеров:

Пример 1: Понимание списка :

[x*2 | x<-[1..10], odd x]

Это выражение возвращает двойные числа всех нечетных чисел в диапазоне от 1 до 10. Очень полезно!

Оказывается, это действительно просто синтаксический сахар для некоторых операций в монаде List. Такое же понимание списка можно записать так:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

Или даже:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

Пример 2: ввод / вывод :

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

Оба примера используют монады, построители вычислений AKA. Общая тема заключается в том, что монада цепочки операций каким-то конкретным, полезным способом. В понимании списка операции объединены в цепочку так, что если операция возвращает список, то следующие операции выполняются для каждого элемента в списке. С другой стороны, монада ввода / вывода выполняет операции последовательно, но передает «скрытую переменную», которая представляет «состояние мира», что позволяет нам писать код ввода / вывода чисто функциональным образом.

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

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

Синтаксис осмысления списка и нотация do являются синтаксическим сахаром для операций цепочки с использованием >>=оператора. Монада - это просто тип, который поддерживает >>=оператор.

Пример 3: парсер

Это очень простой парсер, который анализирует либо строку в кавычках, либо число:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

Операции char, digitи т.д. довольно просты. Они либо совпадают, либо не совпадают. Волшебство - это монада, которая управляет потоком управления: операции выполняются последовательно до тех пор, пока совпадение не завершится, и в этом случае монада возвращается к последнему <|>и пробует следующую опцию. Опять же, способ объединения операций с некоторой дополнительной полезной семантикой.

Пример 4: Асинхронное программирование

Приведенные выше примеры есть в Haskell, но, оказывается, F # также поддерживает монады. Этот пример украден у Дона Сайма :

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

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

В большинстве других языков вам придется явно создать отдельную функцию для строк, которые обрабатывают ответ. asyncМонада в состоянии «раскол» блок на своем и отложить исполнение второй половины. ( async {}Синтаксис указывает, что поток управления в блоке определяется asyncмонадой.)

Как они работают

Так как же монада может делать все эти причудливые вещи управления потоком? Что действительно происходит в do-блоке (или вычислительном выражении, как они называются в F #), так это то, что каждая операция (в основном каждая строка) заключена в отдельную анонимную функцию. Эти функции затем объединяются с помощью bindоператора (пишется >>=на Haskell). Поскольку bindоперация объединяет функции, она может выполнять их так, как считает нужным: последовательно, несколько раз, в обратном порядке, отбрасывать некоторые, выполнять некоторые в отдельном потоке, когда чувствует себя так, и так далее.

В качестве примера, это расширенная версия IO-кода из примера 2:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

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

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

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

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

Подводя итоги

В терминах Haskell монада - это параметризованный тип, который является экземпляром класса типов Monad, который определяет >>=наряду с несколькими другими операторами. С точки зрения непрофессионала, монада - это просто тип, для которого определена >>=операция.

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

Почему монады трудны?

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

Отличный блог о монадах: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


66
Как человек, у которого было много проблем с пониманием монад, я могу сказать, что этот ответ помог ... немного. Тем не менее, есть некоторые вещи, которые я не понимаю. Каким образом понимание списка является монадой? Есть ли расширенная форма этого примера? Еще одна вещь, которая действительно беспокоит меня по поводу большинства объяснений монад, в том числе и этого: разве они продолжают путать "что такое монада?" с "для чего хороша монада?" и "Как реализована монада?" вы перепрыгнули эту акулу, когда написали: «Монада - это просто тип, который поддерживает оператор >> =». Который только что был у меня ...
Бретон

83
Также я не согласен с вашим выводом о том, почему монады сложны. Если сами монады не сложны, тогда вы сможете объяснить, что они из себя представляют, без большого количества багажа. Я не хочу знать о реализации, когда я задаю вопрос «Что такое монада», я хочу знать, что это за зуд, что значит чесать. До сих пор кажется, что ответ таков: «Поскольку авторы haskell являются садомазохистами и решили, что вам нужно сделать что-то глупо сложное для выполнения простых вещей, вам нужно учить монады использовать haskell, а не потому, что они каким-либо образом полезны в самих себя"...
Бретон

70
Но .. это не может быть правдой, не так ли? Я думаю, что монады сложны, потому что никто не может понять, как их объяснить, не увязнув в запутанных деталях реализации. Я имею в виду .. что такое школьный автобус? Это металлическая платформа с устройством спереди, которое потребляет очищенный нефтепродукт, чтобы в цикле приводить в движение несколько металлических поршней, которые в свою очередь вращают коленчатый вал, прикрепленный к некоторым шестерням, которые приводят в движение некоторые колеса. Колеса имеют надувные резиновые мешки вокруг них, которые взаимодействуют с поверхностью из асфальта, чтобы заставить ряд сидений двигаться вперед. Сиденья двигаются вперед, потому что ...
Бретон

130
Я прочитал все это и до сих пор не знаю, что такое монада, кроме того, что программисты на Haskell не понимают этого достаточно хорошо, чтобы объяснить. Примеры не очень помогают, учитывая, что это все, что можно сделать без монад, и этот ответ не проясняет, как монады делают их легче, а только запутывает. Одна часть этого ответа, которая была почти полезной, была там, где был удален синтаксический сахар из примера № 2. Я говорю, что подошел близко, потому что, кроме первой строки, расширение не имеет никакого реального сходства с оригиналом.
Лоуренс Гонсалвес

81
Другая проблема, которая кажется эндемичной для объяснений монад, заключается в том, что она написана на Хаскеле. Я не говорю, что Хаскель - плохой язык - я говорю, что это плохой язык для объяснения монад. Если бы я знал Хаскель, я бы уже понимал монады, поэтому, если вы хотите объяснить монады, начните с использования языка, который люди, не знающие монады, с большей вероятностью поймут. Если вы должны использовать Haskell, вообще не используйте синтаксический сахар - используйте наименьшее, самое простое подмножество языка, которое вы можете, и не предполагайте понимание Haskell IO.
Лоуренс Гонсалвес

712

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

Что такое монада? Краткий ответ: это особый способ объединения операций.

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

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

Это звучит не слишком сложно, верно? Но есть более чем один вид монады. Почему? Как?

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

(Монада, которая не делает ничего особенного, называется «монадой идентичности». Скорее, как функция идентичности, это звучит как совершенно бессмысленная вещь, но оказывается, что это не так… Но это другая история ™.)

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

  • Если каждый шаг возвращает индикатор успеха / неудачи, bind может выполнить следующий шаг, только если предыдущий был успешным. Таким образом, неудачный шаг прерывает всю последовательность «автоматически», без каких-либо условных проверок с вашей стороны. ( Отказ Монада .)

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

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

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

  • Вы можете сделать так, чтобы «дополнительные данные» можно было заменить. Это позволяет вам моделировать деструктивные обновления , фактически не делая деструктивных обновлений. ( Государственная Монада и ее двоюродный брат Писатель Монада .)

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

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

  • Вы можете реализовать «продолжения» как монаду. Это позволяет вам ломать умы людей!

Все это и многое другое возможно с монадами. Конечно, все это также вполне возможно без монад. С помощью монад просто проще .


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

1
По крайней мере, в Haskell оптимизатор отбирает большую часть накладных расходов монад. Таким образом, единственная реальная «стоимость» - это необходимая мощность мозга. (Это немаловажно, если «ремонтопригодность» - это то, что вас волнует.) Но обычно монады облегчают , а не усложняют ситуацию . ( В противном случае, почему бы вам беспокоиться?)
MathematicalOrchid

Я не уверен, поддерживает ли Haskell это, но математически вы можете определить монаду в терминах >> = и return или join и ap. >> = и return делают монады практически полезными, но join и ap дают более интуитивное понимание того, что такое монада.
Список Джереми

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

10
Это первый ответ, который фактически дал мне некоторое представление о том, что, черт возьми, монада. Спасибо, что нашли способ объяснить это!
robotmay

186

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

Например, вы можете создать тип для переноса другого в Haskell:

data Wrapped a = Wrap a

Чтобы обернуть вещи мы определяем

return :: a -> Wrapped a
return x = Wrap x

Для выполнения операций без развертывания, скажем, у вас есть функция f :: a -> b, вы можете сделать это, чтобы поднять эту функцию, чтобы воздействовать на упакованные значения:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

Вот и все, что нужно понять. Тем не менее, оказывается, что есть более общая функция , чтобы сделать этот подъем , который bind:

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bindможет сделать немного больше fmap, но не наоборот. На самом деле, fmapможно определить только с точки зрения bindи return. Итак, при определении монады .. вы даете ее тип (здесь это было Wrapped a), а затем говорите, как ее returnиbind рабочие операции.

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

Для хорошей статьи о том, как монады могут использоваться для введения функциональных зависимостей и, таким образом, управления порядком оценки, как это используется в монаде IO Haskell, ознакомьтесь с IO Inside .

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


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

1
Я не думаю, что это очень хорошее объяснение вообще. Монады просто способ? хорошо, куда? Почему бы мне не использовать класс вместо монады?
Бретон

4
@ mb21: Если вы просто указываете, что скобок слишком много, обратите внимание, что a-> b-> c на самом деле означает только a -> (b-> c). Запись этого конкретного примера в виде (a -> b) -> (Ta -> Tb) - это, строго говоря, просто добавление ненужных символов, но это морально "правильная вещь", так как подчеркивает, что fmap отображает функцию типа a -> b к функции типа Ta -> Tb. Изначально именно так поступают функторы в теории категорий, и именно отсюда возникают монады.
Николай-К

1
Этот ответ вводит в заблуждение. Некоторые монады вообще не имеют «обертки», такие как функции с фиксированным значением.

1
@DanMandel Monads - это шаблоны проектирования, которые предоставляют свою собственную оболочку типа данных. Монады спроектированы таким образом, чтобы абстрагировать шаблонный код. Так как вы называете Monad в своем коде, она делает закулисные вещи, о которых вам не хочется беспокоиться. Подумайте о Nullable <T> или IEnumerable <T>, что они делают за кулисами? Это Монада.
Сксаллай

168

Но вы могли бы изобрести монады!

Sigfpe говорит:

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

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

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


9
… Лучший способ не только в интернете, но и везде. (Оригинальная статья Вадлера «Монады для функционального программирования», о которой я упоминал в своем ответе ниже, также хороша.) Ни один из множества учебников по аналогии не подходит.
ShreevatsaR

13
Этот перевод JavaScript сообщения Sigfpe - это новый лучший способ изучения монад для людей, которые еще не освоили продвинутый Haskell!
Сэм Уоткинс

1
Так я узнал, что такое монада. Прохождение читателя через процесс изобретения концепции часто является лучшим способом обучения концепции.
Джордан

Однако функция, принимающая экранный объект в качестве аргумента и возвращающая его копию с измененным текстом, будет чистой.
Дмитрий Зайцев

87

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

В нотации Haskell написан интерфейс монады

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

Предполагается, что эти операции подчиняются определенным «законам», но это не очень важно: «законы» просто кодифицируют способ, которым должны вести себя разумные реализации операций (в основном это >>=и returnдолжно согласовывать, как значения преобразуются в экземпляры монад и который>>= ассоциативно).

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

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

где []и :являются списочными конструкторами, ++оператором конкатенации, и Justи NothingявляютсяMaybe конструкторы. Обе эти монады инкапсулируют общие и полезные шаблоны вычислений для их соответствующих типов данных (обратите внимание, что ни один из них не имеет ничего общего с побочными эффектами или вводом / выводом).

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


Что именно вы подразумеваете под «отображением функции над ней»?
Casebash

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

3
Монада это не тип данных. Это правило составления функций: stackoverflow.com/a/37345315/1614973
Дмитрий Зайцев

@DmitriZaitsev прав, Монады фактически предоставляют свой собственный тип данных, Монады не типы данных
sksallaj

78

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

Функция высшего порядка - это просто функция, которая принимает функцию в качестве аргумента.

Функтор является любой тип конструкции , Tдля которой существует функция высшего порядка, назовем его map, который преобразовывает функцию типа a -> b( с учетом любых двух типов , aа b) в функцию T a -> T b. Эта mapфункция также должна подчиняться законам идентичности и композиции, чтобы следующие выражения возвращали true для всех pи q(нотация Haskell):

map id = id
map (p . q) = map p . map q

Например, конструктор типа называется Listфунктором, если он снабжен функцией типа, (a -> b) -> List a -> List bкоторая подчиняется законам выше. Единственная практическая реализация очевидна. Результирующая List a -> List bфункция выполняет итерацию по заданному списку, вызывая (a -> b)функцию для каждого элемента, и возвращает список результатов.

Монада по существу только функтор Tс двумя дополнительными методами, join, типа T (T a) -> T a, и unit(иногда называют return, forkили pure) типа a -> T a. Для списков в Haskell:

join :: [[a]] -> [a]
pure :: a -> [a]

Почему это полезно? Потому что вы можете, например, mapнад списком с функцией, которая возвращает список. Joinберет полученный список списков и объединяет их. Listэто монада, потому что это возможно.

Вы можете написать функцию, которая делает map, тоjoin . Эта функция называется bind, или flatMap, или (>>=), или (=<<). Это обычно, как экземпляр монады дается в Haskell.

Монада должна удовлетворять определенным законам, а именно, она joinдолжна быть ассоциативной. Это означает, что если у вас есть значение xтипа, [[[a]]]то join (join x)должно равняться join (map join x). И pureдолжно быть тож для joinтакого join (pure x) == x.


3
небольшое добавление к def «функции более высокого порядка»: они могут принимать функции OR RETURN. Вот почему они «выше», потому что они делают вещи сами с собой.
Кевин выиграл

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

Видео « Брайан Бекман: ​​не бойся монады » следует той же логике.
icc97

48

[Отказ от ответственности: я все еще пытаюсь полностью поглощать монады. Вот что я понял до сих пор. Если это не так, надеюсь, кто-то знающий позвонит мне на ковер.]

Арнар написал:

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

Это именно так. Идея звучит так:

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

    Например, эта дополнительная информация может быть Maybeили IO.

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

    Например, Maybe Intможет быть Just Intили Nothing. Теперь, если вы добавите a Maybe Intк a Maybe Int, оператор проверит, находятся ли они Just Intвнутри s, и, если это так, развернет Ints, передаст им оператор сложения, перевернет полученный результат Intв новый Just Int(который является действительным Maybe Int) и, таким образом, вернуть Maybe Int. Но если один из них был Nothingвнутри, этот оператор сразу же вернется Nothing, что снова является действительным Maybe Int. Таким образом, вы можете притвориться, что ваши Maybe Intчисла просто нормальные, и выполнять с ними регулярные вычисления. Если бы вы получили Nothing, ваши уравнения все равно будут давать правильный результат - без необходимости мусорить проверки Nothingвезде .

Но пример как раз то, что происходит Maybe. Если бы дополнительная информация была a IO, то IOвместо этого вызывался бы этот специальный оператор, определенный для s, и он мог сделать что-то совершенно другое перед выполнением сложения. (Хорошо, добавление двух IO Ints вместе, вероятно, бессмысленно - я еще не уверен.) (Кроме того, если вы обратили внимание на Maybeпример, вы заметили, что «оборачивать значение дополнительными вещами» не всегда правильно. Но это сложно быть точным, правильным и точным, не будучи непостижимым.)

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

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

Но подумайте о последствиях наличия синтаксической поддержки в языке для идеи шаблона: вместо того, чтобы читать книгу « Банда четырех» и запоминать конструкцию конкретного шаблона, вы просто пишете код, который реализует этот шаблон в агностике, общий способ один раз, и тогда вы сделали! Затем вы можете повторно использовать этот шаблон, например Visitor, Strategy, Façade или любой другой, просто украсив им операции в вашем коде, без необходимости повторной реализации его снова и снова!

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


12
Иногда объяснение от «ученика» (такого как ты) более важно для другого ученика, чем объяснение, исходящее от эксперта. Ученики думают одинаково :)
Адриан

Что делает что-то монадой, так это существование функции с типом M (M a) -> M a. Тот факт, что вы можете превратить это в один из типов, M a -> (a -> M b) -> M bделает их полезными.
Джереми Лист

«Монада» примерно означает «шаблон» ... нет.
Спасибо

44

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

Есть три вопроса, на которые нужно ответить, чтобы понять монады:

  1. Зачем вам нужна монада?
  2. Что такое монада?
  3. Как реализована монада?

Как я отмечал в моих первоначальных комментариях, слишком много объяснений монады попадают в вопрос № 3, прежде чем он действительно адекватно охватывает вопрос 2 или вопрос 1.

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

Чистые функциональные языки, такие как Haskell, отличаются от императивных языков, таких как C или Java, тем, что чисто функциональные программы не обязательно выполняются в определенном порядке, по одному шагу за раз. Программа на Haskell больше похожа на математическую функцию, в которой вы можете решить «уравнение» в любом количестве возможных порядков. Это дает ряд преимуществ, среди которых заключается в том, что исключает возможность ошибок определенного рода, особенно тех, которые связаны с такими вещами, как «состояние».

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

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

consolestate MyConsole = new consolestate;

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

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

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

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

Это действительно был бы более удобный способ написать это. Как мы это делаем, хотя?

Что такое монада?

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

Как реализована монада?

Посмотрите другие ответы, которые, кажется, совершенно свободно прыгать в детали этого.


Секвенирование - не единственная причина для определения монады. Монада - это просто любой функтор, который имеет привязку и возврат. Привязка и возврат дают вам последовательность. Но они дают и другие вещи. Кроме того, обратите внимание, что ваш любимый императивный язык - это по сути модная монада IO с OO-классами. Упрощение определения монад означает простоту использования шаблона интерпретатора - определите dsl как монаду и интерпретируйте ее!
номен


38

Ответив на этот вопрос несколько лет назад, я считаю, что могу улучшить и упростить этот ответ с помощью ...

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

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

(x -> y) >> (y -> z)

Эту конструкцию можно улучшить, реструктурировав входные данные, чтобы легче было запрашивать соответствующие состояния. Таким образом, вместо того, чтобы просто yзначение может стать Mbтаким, как, например, (is_OK, b)если yвключено понятие валидности.

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

(Ma -> Mb) >> (Mb -> Mc)

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

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

Для достижения этой экстернализации мы могли бы использовать функцию, bind (>>=), для выполнения compositionвместо compose. Таким образом, вместо простой передачи значений из выходных данных одной функции на входные данные другой Bindбудет проверять Mчасть Maи решать, применять ли составную функцию к и и как a. Конечно, функция bindбудет определена специально для нашего конкретного Mслучая, чтобы иметь возможность проверять ее структуру и выполнять любые приложения, которые мы хотим. Тем не менее, оно aможет быть чем угодно, поскольку bindпросто пропускаетa неинспектированное составной функции, когда оно определяет необходимое применение. Кроме того, сами составные функции больше не должны иметь дело сMчасть структуры ввода либо, упрощая их. Следовательно ...

(a -> Mb) >>= (b -> Mc) или более кратко Mb >>= (b -> Mc)

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

Следовательно, монада это три вещи:

  1. Mоболочка для проведения монады соответствующей информации,
  2. bindфункции реализованы , чтобы использовать эту информацию оболочки в ее применении в составе функций к стоимости контента (ы) , которые он находит внутри оболочки, и
  3. составные функции формы, a -> Mbпроизводящие результаты, которые включают монадические данные управления.

Вообще говоря, входные данные для функции гораздо более строгие, чем ее выходные данные, которые могут включать в себя такие вещи, как условия ошибки; следовательно, Mbструктура результата, как правило, очень полезна. Например, оператор деления не возвращает число, когда делитель 0.

Кроме того, monads может включать в себя функции переноса, которые переносят значения aв монадический тип Ma, и общие функции a -> bв монадические функции, a -> Mbзаключая их результаты после применения. Конечно, bindтакие функции переноса являются специфическими для M. Пример:

let return a = [a]
let lift f a = return (f a)

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

Данный...

M_ 
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)

Затем...

Left Identity  : (return a) >>= f === f a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativityозначает, что bindсохраняет порядок оценки независимо от того, когда bindприменяется. То есть, в определении Associativityвыше, в силу ранней оценки из скобки bindingиз fи gприведет лишь к функции , которая ожидает Maв целях завершения bind. Следовательно, оценка Maдолжна быть определена до того, как ее значение может быть применено, fи этот результат, в свою очередь, будет применен к g.


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

Это самое краткое и ясное объяснение монад, которые я когда-либо читал / смотрел / слышал. Спасибо!
Джеймс

Между Монадой и Моноидом есть важное различие. Monad - это правило для «составления» функций между различными типами, поэтому они не образуют двоичную операцию, как это требуется для Monoids, смотрите здесь для более подробной информации: stackoverflow.com/questions/2704652/…
Дмитрий Зайцев

Да. Ты прав. Ваша статья была у меня над головой :). Тем не менее, я нашел это лечение очень полезным (и добавил его к моему в качестве направления для других). Спасибо за внимание: stackoverflow.com/a/7829607/1612190
Джордж

2
Возможно, вы спутали теорию алгебраических групп с теорией категорий , где Монада откуда. Первая - это теория алгебраических групп, которая не связана.
Дмитрий Зайцев

37

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

«Возможно, монада» по существу эквивалентна «обнуляемым типам» в Visual Basic / C #. Он принимает необнуляемый тип «T» и преобразует его в «Nullable <T>», а затем определяет, что означают все двоичные операторы для Nullable <T>.

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

Они называются «монадами», а не более простым для понимания именем «операторов типа» по нескольким причинам:

  1. Монады имеют ограничения на то, что они могут делать (подробности см. В определении).
  2. Эти ограничения, а также тот факт, что задействованы три операции, соответствуют структуре чего-то, называемого монадой в теории категорий, которая является неясной областью математики.
  3. Они были разработаны сторонниками «чистых» функциональных языков
  4. Сторонники чисто функциональных языков, таких как малопонятные разделы математики
  5. Поскольку математика неясна, а монады связаны с определенными стилями программирования, люди склонны использовать слово «монада» как своего рода секретное рукопожатие. Из-за этого никто не удосужился инвестировать в лучшее имя.

1
Монады не были «разработаны», они применялись из одной области (теория категорий) в другую (ввод / вывод в чисто функциональных языках программирования). Ньютон «проектировал» исчисление?
Джаред Апдайк

1
Пункты 1 и 2 выше верны и полезны. Пункты 4 и 5 являются своего рода ad hominem, даже если они более или менее верны. Они действительно не помогают объяснить монады.
Джаред Апдайк

13
Re: 4, 5: «Секретное рукопожатие» - это красная сельдь. Программирование полно жаргона. Хаскелл просто называет вещи тем, что есть, не притворяясь, что что-то заново открыл. Если он уже существует в математике, зачем придумывать для него новое имя? Название на самом деле не является причиной, по которой люди не получают монады; они тонкое понятие. Средний человек, вероятно, понимает сложение и умножение, почему они не понимают абелеву группу? Потому что он более абстрактный и общий, и этот человек не сделал работу, чтобы обернуть голову вокруг концепции. Изменение имени не поможет.
Джаред Апдайк

16
Вздох ... Я не нападаю на Хаскелла ... Я пошутил. Так что я не совсем понимаю, что такое "ad hominem". Да, исчисление было «разработано». Вот почему, например, учащимся по исчислению преподают нотацию Лейбница, а не то, что использовал Ньютон. Лучший дизайн. Хорошие имена помогают понять многое. Если бы я назвал абелевские группы «растянутыми морщинами», у вас могут возникнуть проблемы с пониманием меня. Возможно, вы говорите «но это имя бессмысленно», никто бы никогда не назвал их так. Для людей, которые никогда не слышали о теории категорий, «монада» звучит как чепуха.
Скотт Вишневски

4
@ Скотт: извините, если мои обширные комментарии дали мне понять, что я защищаюсь от Хаскелла. Мне нравится ваш юмор по поводу секретного рукопожатия, и вы заметите, я сказал, что это более или менее верно. :-) Если бы вы назвали абелевы группы «растянутыми стручками морщин», вы бы совершили ту же ошибку, пытаясь дать монадам «лучшее имя» (см. F # «выражения для вычислений»): этот термин существует, и люди, которым все равно, знают, что такое монады. есть, но не то, что "теплые нечеткие вещи" (или "выражения вычислений"). Если я правильно понимаю, что вы используете термин «оператор типа», есть много операторов другого типа, кроме монад.
Джаред Апдайк

35

(См. Также ответы в разделе Что такое монада? )

Хорошей мотивацией для Монад является sigfpe (Дэн Пипони), « Вы могли бы изобрести монады» ! (А может быть, у вас уже есть) . Существует множество других учебных пособий по монадам, многие из которых ошибочно пытаются объяснить монады «простыми терминами», используя различные аналогии: это ошибка учебного пособия по монадам. ; избежать их.

Как говорит DR MacIver в Расскажите нам, почему ваш язык отстой :

Итак, вещи, которые я ненавижу в Haskell:

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

Вы говорите, что понимаете монаду Может быть? Хорошо, ты уже в пути. Просто начните использовать другие монады, и рано или поздно вы поймете, что такое монады в целом.

[Если вы ориентированы на математику, вы можете игнорировать десятки учебных пособий и выучить определение или следовать лекциям по теории категорий. :) Основная часть определения состоит в том, что Monad M включает в себя «конструктор типов», который определяет для каждого существующий тип "T", новый тип "MT" и некоторые способы перехода назад и вперед между "обычными" типами и типами "M".]

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


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

+1 за "ошибку учебника монады". Учебники по монадам сродни наличию нескольких учебников, пытающихся объяснить концепцию целых чисел. Один учебник скажет: «1 похоже на яблоко»; другой урок гласит: «2 как груша»; третий говорит: «3 в основном апельсин». Но вы никогда не получите полную картину из любого учебника. Из этого я понял, что монады - это абстрактное понятие, которое можно использовать для самых разных целей.
stakx - больше не участвует

@stakx: Да, правда. Но я не имел в виду, что монады - это абстракция, которую вы не можете изучать или не должны изучать; только то, что лучше всего изучать это после того, как вы увидели достаточно конкретных примеров, чтобы воспринимать одну основную абстракцию. Смотрите мой другой ответ здесь .
ShreevatsaR

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

Упоминание только о конструкторе типов является неполным: stackoverflow.com/a/37345315/1614973
Дмитрий Зайцев

23

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

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

Однако, сталкиваясь с программным потоком, многие разработчики не сталкивались с гораздо большим количеством конструкций, чем if, switch / case, do, while, goto (grr) и (возможно) замыкания.

Итак, монада - это просто конструкция потока управления. Лучшей фразой для замены монады будет «тип управления».

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

Например, монада "если":

if( clause ) then block

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

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

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

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


14

Мой любимый учебник Monad:

http://www.haskell.org/haskellwiki/All_About_Monads

(из 170 000 просмотров в поиске Google "учебник монад"!)

@Stu: смысл монад в том, чтобы позволить вам добавить (обычно) последовательную семантику в чистый код; Вы даже можете создавать монады (используя Monad Transformers) и получать более интересную и сложную комбинированную семантику, например, например, анализ с обработкой ошибок, общим состоянием и журналированием. Все это возможно в чистом коде, монады просто позволяют абстрагировать его и повторно использовать в модульных библиотеках (всегда хороших в программировании), а также предоставляют удобный синтаксис, чтобы сделать его обязательным.

На Haskell уже есть перегрузка операторов [1]: он использует классы типов так же, как можно было бы использовать интерфейсы в Java или C #, но Haskell просто позволяет также использовать не алфавитно-цифровые токены, такие как + && и>, в качестве идентификаторов инфиксов. Это только перегрузка оператора, если вы имеете в виду «перегрузка точки с запятой» [2]. Звучит как чёрная магия, и возникает проблема с «перегрузкой точки с запятой» (представьте, что предприимчивые хакеры Perl узнают об этой идее), но дело в том, что без монад нет точки с запятой, поскольку чисто функциональный код не требует или не допускает явной последовательности.

Все это звучит намного сложнее, чем нужно. Статья sigfpe довольно крутая, но для объяснения она использует Haskell, что не решает проблему курицы и яйца, заключающуюся в том, чтобы понять Haskell, чтобы обмануть Monads, и понять Monads, чтобы обмануть Haskell.

[1] Это отдельная проблема от монад, но монады используют функцию перегрузки операторов Haskell.

[2] Это также упрощение, так как оператор для связывания монадических действий: >> = (произносится «связать»), но есть синтаксический сахар («до»), который позволяет вам использовать фигурные скобки и точки с запятой и / или отступы и символы новой строки.


9

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

Если вы используете императивный язык и пишете некоторые выражения по порядку, код ВСЕГДА выполняется именно в таком порядке.

И в простом случае, когда вы используете монаду, вы чувствуете то же самое - вы определяете список выражений, которые встречаются по порядку. Кроме того, в зависимости от того, какую монаду вы используете, ваш код может выполняться по порядку (как в монаде IO), параллельно по нескольким элементам одновременно (как в монаде списка), он может останавливаться на полпути (как в монаде Maybe) , он может приостановиться на полпути, чтобы быть возобновленным позже (как в монаде Возобновления), он может перемотаться и начаться с начала (как в монаде Транзакции), или он может перемотаться на полпути, чтобы попробовать другие варианты (как в монаде Логики) ,

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

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


9

Я все еще новичок в монадах, но я подумал, что поделюсь ссылкой, которую я нашел и которую очень приятно читать (С ФОТОГРАФИЯМИ!): Http://www.matusiak.eu/numerodix/blog/2012/3/11/ монады для обывателя / (без принадлежности)

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

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


Примеры Python позволили легко понять! Спасибо, что поделился.
Райан Эфенди

8

В дополнение к превосходным ответам, приведенным выше, позвольте мне предложить вам ссылку на следующую статью (Патрик Томсон), в которой объясняются монады, связывая концепцию с библиотекой JavaScript jQuery (и ее способ использования «цепочки методов» для манипулирования DOM). : jQuery - это монада

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


Если вы используете jQuery, это объяснение может быть очень полезным, особенно если ваш Haskell не силен
byteclub

10
JQuery не является монадой. Связанная статья неверна.
Тони Моррис

1
Быть «решительным» не очень убедительно. Для некоторого полезного обсуждения по теме посмотрите, Является ли jQuery монадой - Переполнение стека
nealmcb

1
См. Также Даглас Крэкфорд, Google Talk Monads и Gonads и его Javascript-код для создания модад, рассказывающий о похожем поведении библиотек и обещаний AJAX: douglascrockford / monad · GitHub
nealmcb


7

Монада - это способ объединения вычислений, которые имеют общий контекст. Это похоже на построение сети труб. При построении сети данные по ней не передаются. Но когда я закончил объединять все биты вместе с 'bind' и 'return', тогда я вызываю что-то вроде, runMyMonad monad dataи данные проходят через каналы.


1
Это больше похоже на Applicative, чем на Monad. С Monads вы должны получить данные из каналов, прежде чем сможете выбрать следующий канал для подключения.
Пикер

да, вы описываете Аппликатив, а не Монаду. Monad - это построение следующего сегмента трубы на месте, в зависимости от данных, которые достигли этой точки внутри трубы.
Уилл Несс

6

На практике monad - это пользовательская реализация оператора композиции функций, которая заботится о побочных эффектах и ​​несовместимых входных и возвращаемых значениях (для формирования цепочки).


5

Если я правильно понял, IEnumerable является производным от монад. Интересно, может ли это быть интересным углом зрения для тех из нас, кто живет в мире C #?

Для чего это стоит, вот несколько ссылок на учебники, которые мне помогли (и нет, я до сих пор не понял, что такое монады).


5

Две вещи, которые помогли мне лучше всего, узнав об этом, были:

Глава 8, «Функциональные парсеры», из книги Грэма Хаттона « Программирование на Хаскеле» . На самом деле это вообще не касается монад, но если вы сможете проработать главу и по-настоящему понять все в ней, особенно то, как оценивается последовательность операций связывания, вы поймете внутренности монад. Ожидайте, что это займет несколько попыток.

Учебник Все о монадах . Это дает несколько хороших примеров их использования, и я должен сказать, что аналогия в Appendex я работал для меня.


5

Monoid, по-видимому, гарантирует, что все операции, определенные для Monoid и поддерживаемого типа, всегда будут возвращать поддерживаемый тип внутри Monoid. Например, любое число + любое число = число, без ошибок.

Принимая во внимание, что деление принимает два дробных числа и возвращает дробное число, которое определило деление на ноль как бесконечность в haskell, почему-то (что иногда является дробным некоторым образом) ...

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

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

В Haskell main имеет тип IO () или IO [()], это странное распределение, и я не буду его обсуждать, но вот что я думаю:

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

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

По сути, Monads - это подсказка для компилятора: «эй, вы знаете эту функцию, которая возвращает число здесь, она на самом деле не всегда работает, иногда она может генерировать число, а иногда вообще ничего, просто держите это в разум". Зная это, если вы пытаетесь утвердить монадическое действие, монадическое действие может действовать как исключение времени компиляции, говоря «эй, это на самом деле не число, это МОЖЕТ быть числом, но вы не можете предположить это, сделать что-то чтобы гарантировать, что поток является приемлемым. " что предотвращает непредсказуемое поведение программы - в значительной степени.

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

Самая большая причина, по которой я мог придумать Monads, - это посмотреть на процедурный / ООП-код, и вы заметите, что вы не знаете, где начинается или заканчивается программа, все, что вы видите, это много прыжков и много математики , магия и ракеты. Вы не сможете поддерживать его, и, если сможете, вы потратите довольно много времени на то, чтобы сосредоточиться на всей программе, прежде чем сможете понять какую-либо ее часть, потому что модульность в этом контексте основана на взаимозависимых «разделах». кода, где код оптимизирован, чтобы быть как можно более связанными для обеспечения эффективности / взаимосвязи. Монады очень конкретны и хорошо определены по определению и обеспечивают возможность анализа потока программы и выделения частей, которые трудно анализировать - так как они сами являются монадами. Монада кажется " или уничтожить вселенную, или даже исказить время - мы понятия не имеем и не имеем никаких гарантий, что это то, что есть. Монада ГАРАНТИРУЕТ, ЧТО ЭТО ТАКОЕ. что очень сильно. или уничтожить вселенную, или даже исказить время - мы понятия не имеем и не имеем никаких гарантий, что это то, что есть. Монада ГАРАНТИРУЕТ, ЧТО ЭТО ТАКОЕ. что очень сильно.

Все вещи в «реальном мире» кажутся монадами в том смысле, что они связаны определенными наблюдаемыми законами, предотвращающими путаницу. Это не означает, что мы должны имитировать все операции этого объекта для создания классов, вместо этого мы можем просто сказать «квадрат есть квадрат», ничего, кроме квадрата, даже не прямоугольника и не круга, и «квадрат имеет площадь длины одного из его существующих измерений, умноженных на себя. Независимо от того, какой у вас квадрат, если это квадрат в 2D-пространстве, его площадь абсолютно не может быть ничем, кроме его длины в квадрате, это почти тривиально доказать. Это очень сильно, потому что нам не нужно делать утверждения, чтобы убедиться, что наш мир такой, какой он есть, мы просто используем значение реальности, чтобы не допустить срыва наших программ.

Я гарантированно ошибаюсь, но я думаю, что это может кому-то помочь, так что, надеюсь, это кому-то поможет.


5

В контексте Scala вы найдете следующее определение. По сути, flatMap (или связывание) является «ассоциативным» и существует идентичность.

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
      x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))

    // for every parameter X and x, there exists an id
    // such that the following holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

Например

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)

// Observe these are identical. Since Option is a Monad 
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)

scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)


// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)

// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

ПРИМЕЧАНИЕ. Строго говоря, определение монады в функциональном программировании не совпадает с определением монады в теории категорий , которое определяется поочередно mapи flatten. Хотя они являются своего рода эквивалентом при определенных отображениях. Эти презентации очень хороши: http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category


5

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

Рассмотрим эти три функции в псевдокоде:

f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x)          := <x, "">

fпринимает упорядоченную пару формы <x, messages>и возвращает упорядоченную пару. Первый элемент остается нетронутым и добавляется "called f. "ко второму элементу. То же самое с g.

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

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">

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

Вы предпочитаете писать более простые функции:

f(x)    := <x, "called f. ">
g(x)    := <x, "called g. ">
wrap(x) := <x, "">

Но посмотрите, что происходит, когда вы их составляете:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">

Проблема в том, что передача пары в функцию не дает того, что вы хотите. Но что, если бы вы могли вставить пару в функцию:

  feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">

Читайте , feed(f, m)как «корм mв f». Для того, чтобы накормить пару <x, messages>в функцию f, чтобы пройти x в f, получить <y, message>из f, и вернуться <y, messages message>.

feed(f, <x, messages>) := let <y, message> = f(x)
                          in  <y, messages message>

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

Во-первых: если вы переносите значение, а затем передаете полученную пару в функцию:

  feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
  in  <y, "" message>
= let <y, message> = <x, "called f. ">
  in  <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)

Это то же самое, что передать значение в функцию.

Второе: если вы кормите пару в wrap:

  feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
  in  <y, messages message>
= let <y, message> = <x, "">
  in  <y, messages message>
= <x, messages "">
= <x, messages>

Это не меняет пару.

Третье: если вы определяете функцию, которая принимает xи передает g(x)в f:

h(x) := feed(f, g(x))

и корми пару в нее:

  feed(h, <x, messages>)
= let <y, message> = h(x)
  in  <y, messages message>
= let <y, message> = feed(f, g(x))
  in  <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
  in  <y, messages message>
= let <y, message> = let <z, msg> = f(x)
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
  in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))

Это то же самое, что вводить пару gи вводить полученную пару f.

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

Какой тип стоимости <x, "called f. ">? Ну, это зависит от того, какой тип стоимости x. Если xимеет тип t, то ваша пара является значением типа «пара tи строка». Назовите этот тип M t.

Mявляется конструктором типа: Mодин не относится к типу, но M _относится к типу, когда вы заполняете пробел с типом. An M intявляется парой типа int и строки. An M stringпредставляет собой пару строки и строки. И т.п.

Поздравляем, вы создали монаду!

Формально твоя монада - это кортеж <M, feed, wrap>.

Монада - это кортеж, в <M, feed, wrap>котором:

  • M это конструктор типа
  • feedпринимает (функция, которая принимает tи возвращает M u) M tи возвращает и возвращает M u.
  • wrapпринимает vи возвращает M v.

t, uи vявляются любыми тремя типами, которые могут или не могут быть одинаковыми. Монада удовлетворяет трем свойствам, которые вы доказали для своей конкретной монады:

  • Подача упакованного tв функцию - это то же самое, что и передача развернутого tв функцию.

    Формально: feed(f, wrap(x)) = f(x)

  • Кормление M tв wrapничего не делает для M t.

    Формально: feed(wrap, m) = m

  • Ввод M t(вызов m) в функцию, которая

    • передает tвg
    • получает M u(позвонить n) отg
    • питается nвf

    такой же как

    • кормление mвg
    • получать nотg
    • кормление nвf

    Формально: feed(h, m) = feed(f, feed(g, m))гдеh(x) := feed(f, g(x))

Как правило, feedназывается bind(AKA >>=в Haskell) и wrapназывается return.


5

Я постараюсь объяснить Monadв контексте Haskell.

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

Допустим, у нас есть две функции: g :: Int -> Stringи f :: String -> Bool.

Мы можем сделать (f . g) x, что так же, как f (g x), где xэто Intзначение.

При выполнении композиции / применении результата одной функции к другой важно, чтобы типы совпадали. В приведенном выше случае тип возвращаемого результата gдолжен совпадать с типом, принятымf .

Но иногда значения находятся в контекстах, и это упрощает выравнивание типов. (Наличие значений в контекстах очень полезно. Например, Maybe Intтип представляет Intзначение, которого может не быть, IO Stringтип представляет Stringзначение, которое существует в результате выполнения некоторых побочных эффектов.)

Допустим, у нас теперь есть g1 :: Int -> Maybe Stringи f1 :: String -> Maybe Bool. g1и f1очень похожи gи fсоответственно.

Мы не можем сделать, (f1 . g1) xили f1 (g1 x), где xэто Intзначение. Тип возвращаемого результата g1не соответствует f1ожидаемому.

Мы могли бы сочинять fи gс .оператором, но теперь мы не можем сочинять f1и g1с оператором .. Проблема в том, что мы не можем напрямую передать значение в контексте функции, которая ожидает значение, которое не находится в контексте.

Разве не было бы неплохо, если бы мы ввели оператор для компоновки g1и так f1, чтобы мы могли писать (f1 OPERATOR g1) x? g1возвращает значение в контексте. Значение будет вырвано из контекста и применено к f1. И да, у нас есть такой оператор. Это <=<.

У нас также есть >>= оператор, который делает для нас точно такую ​​же вещь, хотя и в несколько ином синтаксисе.

Мы пишем: g1 x >>= f1. g1 xэто Maybe Intзначение. >>=Оператор помогает принять это Intзначение из «возможно, не-там» контекста, и применить его к f1. Результат f1, который является Maybe Bool, будет результатом всей >>=операции.

И, наконец, почему это Monadполезно? Поскольку Monadкласс типа , который определяет >>=оператор, очень много такие же , как Eqкласс типа , который определяет ==и /=оператор.

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

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



IOW, Monad - это обобщенный протокол вызова функций.
Уилл Несс

Твой ответ самый полезный на мой взгляд. Хотя я должен сказать, что думаю, что акцент должен делаться на том факте, что функции, о которых вы говорите, не просто включают значения в контексты, они активно помещают значения в контексты. Так, например, функция f :: ma -> mb будет очень легко составлять другую функцию, g :: mb -> m c. Но монады (в частности, связывание) позволяют нам постоянно составлять функции, которые помещают свои входные данные в один и тот же контекст, и нам не нужно сначала извлекать значение из этого контекста (что будет эффективно удалять информацию из значения)
Джеймс

@ Джеймс, я думаю, что это должно быть акцентом для функторов?
Джонас

@Jonas Думаю, я не объяснил ничего подобного. Когда я говорю, что функции помещают значения в контексты, я имею в виду, что они имеют тип (a -> mb). Они очень полезны, так как помещение значения в контекст добавляет новую информацию к нему, но обычно было бы трудно связать a (a -> mb) и a (b -> mc) вместе, так как мы не можем просто извлечь значение контекста. Таким образом, мы должны были бы использовать некоторый запутанный процесс, чтобы разумно связать эти функции в зависимости от конкретного контекста, и монады просто позволяют нам делать это согласованным образом, независимо от контекста.
Джеймс

5

ТЛ; др

{-# LANGUAGE InstanceSigs #-}

newtype Id t = Id t

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

пролог

Оператор приложения $функций

forall a b. a -> b

канонически определен

($) :: (a -> b) -> a -> b
f $ x = f x

infixr 0 $

с точки зрения приложения функции Haskell f x( infixl 10).

Композиция .определяется с точки зрения $как

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x

infixr 9 .

и удовлетворяет эквивалентности forall f g h.

     f . id  =  f            :: c -> d   Right identity
     id . g  =  g            :: b -> c   Left identity
(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.является ассоциативным и idявляется его правой и левой идентичностью.

Тройка Клейсли

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

Функтор - это конструктор fтипа * -> *с экземпляром класса типа функтор.

{-# LANGUAGE KindSignatures #-}

class Functor (f :: * -> *) where
   map :: (a -> b) -> (f a -> f b)

В дополнение к следующему статически обязательному протоколу типов экземпляры класса функторов должны подчиняться законам алгебраических функторов. forall f g.

       map id  =  id           :: f t -> f t   Identity
map f . map g  =  map (f . g)  :: f a -> f c   Composition / short cut fusion

Функторные вычисления имеют тип

forall f t. Functor f => f t

Вычисление c rсостоит из результатов r в контексте c .

Унарные монадические функции или стрелки Клейсли имеют тип

forall m a b. Functor m => a -> m b

Стрелки Клейси - это функции, которые принимают один аргумент aи возвращают монадическое вычисление m b.

Монады канонически определены в терминах тройки Клейсли forall m. Functor m =>

(m, return, (=<<))

реализован как класс типа

class Functor m => Monad m where
   return :: t -> m t
   (=<<)  :: (a -> m b) -> m a -> m b

infixr 1 =<<

Идентичность Клейла return является Клейл стрелок , которая способствует значению tв монадическую контексте m. Приложение Extension или Kleisli =<< применяет стрелку Kleisli a -> m bк результатам вычислений m a.

Состав Клейсли <=< определяется с точки зрения расширения как

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x

infixr 1 <=<

<=< Составляет две стрелки Клейсли, применяя левую стрелку к результатам применения правой стрелки.

Экземпляры класса типа монады должны подчиняться законам монады , наиболее элегантно изложенным в терминах композиции Клейсли:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identity
   return <=< g  =  g                :: b -> m c   Left identity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d   Associativity

<=<является ассоциативным и returnявляется его правой и левой идентичностью.

тождественность

Тип личности

type Id t = t

это тождественная функция на типах

Id :: * -> *

Интерпретируется как функтор,

   return :: t -> Id t
=      id :: t ->    t

    (=<<) :: (a -> Id b) -> Id a -> Id b
=     ($) :: (a ->    b) ->    a ->    b

    (<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
=     (.) :: (b ->    c) -> (a ->    b) -> (a ->    c)

В каноническом Хаскеле тождественная монада определена

newtype Id t = Id t

instance Functor Id where
   map :: (a -> b) -> Id a -> Id b
   map f (Id x) = Id (f x)

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

вариант

Тип опции

data Maybe t = Nothing | Just t

кодирует вычисления, Maybe tкоторые не обязательно дают результат t, вычисления, которые могут «потерпеть неудачу». Опция монада определена

instance Functor Maybe where
   map :: (a -> b) -> (Maybe a -> Maybe b)
   map f (Just x) = Just (f x)
   map _ Nothing  = Nothing

instance Monad Maybe where
   return :: t -> Maybe t
   return = Just

   (=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
   f =<< (Just x) = f x
   _ =<< Nothing  = Nothing

a -> Maybe bприменяется к результату, только если Maybe aдает результат.

newtype Nat = Nat Int

Натуральные числа могут быть закодированы как целые числа, большие или равные нулю.

toNat :: Int -> Maybe Nat
toNat i | i >= 0    = Just (Nat i)
        | otherwise = Nothing

Натуральные числа не замкнуты при вычитании.

(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)

infixl 6 -?

Опция monad охватывает базовую форму обработки исключений.

(-? 20) <=< toNat :: Int -> Maybe Nat

Список

Монада списка, над типом списка

data [] t = [] | t : [t]

infixr 5 :

и его аддитивная моноидная операция «добавить»

(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[]       ++ ys = ys

infixr 5 ++

кодирует нелинейные вычисления, [t]дающие естественное количество 0, 1, ...результатов t.

instance Functor [] where
   map :: (a -> b) -> ([a] -> [b])
   map f (x : xs) = f x : map f xs
   map _ []       = []

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> [a] -> [b]
   f =<< (x : xs) = f x ++ (f =<< xs)
   _ =<< []       = []

Расширение =<<объединяет ++все списки, [b]полученные в результате применения f xстрелки Клейсли, a -> [b]к элементам [a]в один список результатов [b].

Пусть правильные делители натурального числа nбудут

divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]

divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)

тогда

forall n.  let { f = f <=< divisors } in f n   =   []

При определении класса типа монады вместо расширения =<<стандарт Haskell использует свой flip, оператор связывания>>= .

class Applicative m => Monad m where
   (>>=) :: forall a b. m a -> (a -> m b) -> m b

   (>>) :: forall a b. m a -> m b -> m b
   m >> k = m >>= \ _ -> k
   {-# INLINE (>>) #-}

   return :: a -> m a
   return = pure

Для простоты в этом объяснении используется иерархия классов типов

class              Functor f
class Functor m => Monad m

В Хаскеле текущая стандартная иерархия

class                  Functor f
class Functor p     => Applicative p
class Applicative m => Monad m

потому что не только каждая монада является функтором, но и каждая аппликативная функция является функтором, и каждая монада также является аппликативной.

Используя монаду списка, императивный псевдокод

for a in (1, ..., 10)
   for b in (1, ..., 10)
      p <- a * b
      if even(p)
         yield p

примерно переводится в блок do ,

do a <- [1 .. 10]
   b <- [1 .. 10]
   let p = a * b
   guard (even p)
   return p

эквивалентное понимание монады ,

[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]

и выражение

[1 .. 10] >>= (\ a ->
   [1 .. 10] >>= (\ b ->
      let p = a * b in
         guard (even p) >>       -- [ () | even p ] >>
            return p
      )
   )

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

let x = v in e    =   (\ x -> e)  $  v   =   v  &  (\ x -> e)
do { r <- m; c }  =   (\ r -> c) =<< m   =   m >>= (\ r -> c)

где

(&) :: a -> (a -> b) -> b
(&) = flip ($)

infixl 0 &

Функция охраны определена

guard :: Additive m => Bool -> m ()
guard True  = return ()
guard False = fail

где тип блока или «пустой кортеж»

data () = ()

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

class Monad m => Additive m where
   fail  :: m t
   (<|>) :: m t -> m t -> m t

infixl 3 <|>

instance Additive Maybe where
   fail = Nothing

   Nothing <|> m = m
   m       <|> _ = m

instance Additive [] where
   fail = []
   (<|>) = (++)

где failи <|>образуют моноидforall k l m.

     k <|> fail  =  k
     fail <|> l  =  l
(k <|> l) <|> m  =  k <|> (l <|> m)

и failявляется поглощающим / уничтожающим нулевым элементом аддитивных монад

_ =<< fail  =  fail

Если в

guard (even p) >> return p

even pистинно, тогда сторож производит [()], и, по определению >>, функцию локальной константы

\ _ -> return p

применяется к результату (). Если false, то охранник выдает монаду list fail( []), которая не дает результата для стрелки Клейсли, которую нужно применить >>, поэтому pона пропускается.

государственный

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

Состояние процессора является функцией

forall st t. st -> (t, st)

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

Тип государственных процессоров обычно называется

type State st t = st -> (t, st)

Монада процессора состояний - это добрый * -> *функтор State st. Клейсли стрелки монады состояния процессора являются функциями

forall st a b. a -> (State st) b

В каноническом Haskell определяется ленивая версия монады процессора состояний

newtype State st t = State { stateProc :: st -> (t, st) }

instance Functor (State st) where
   map :: (a -> b) -> ((State st) a -> (State st) b)
   map f (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  (f x, s1)

instance Monad (State st) where
   return :: t -> (State st) t
   return x = State $ \ s -> (x, s)

   (=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
   f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  stateProc (f x) s1

Процессор состояния запускается путем предоставления начального состояния:

run :: State st t -> st -> (t, st)
run = stateProc

eval :: State st t -> st -> t
eval = fst . run

exec :: State st t -> st -> st
exec = snd . run

Доступ к состоянию обеспечивается примитивами getи putметодами абстрагирования по монадам с состоянием :

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}

class Monad m => Stateful m st m -> st where
   get :: m st
   put :: st -> m ()

m -> stобъявляет функциональную зависимость типа состояния stот монады m; что State t, например, определит тип состояния, который будет tуникальным.

instance Stateful (State st) st where
   get :: State st st
   get = State $ \ s -> (s, s)

   put :: st -> State st ()
   put s = State $ \ _ -> ((), s)

с типом устройства, используемым аналогично voidв C.

modify :: Stateful m st => (st -> st) -> m ()
modify f = do
   s <- get
   put (f s)

gets :: Stateful m st => (st -> t) -> m t
gets f = do
   s <- get
   return (f s)

gets часто используется с полями доступа к записи.

Монад состояния, эквивалентный переменной threading

let s0 = 34
    s1 = (+ 1) s0
    n = (* 12) s1
    s2 = (+ 7) s1
in  (show n, s2)

где s0 :: Int- одинаково прозрачный, но бесконечно более элегантный и практичный

(flip run) 34
   (do
      modify (+ 1)
      n <- gets (* 12)
      modify (+ 7)
      return (show n)
   )

modify (+ 1)является вычислением типа State Int (), за исключением его эффекта, эквивалентного return ().

(flip run) 34
   (modify (+ 1) >>
      gets (* 12) >>= (\ n ->
         modify (+ 7) >>
            return (show n)
      )
   )

Монадический закон ассоциативности можно записать в терминах >>= forall m f g.

(m >>= f) >>= g  =  m >>= (\ x -> f x >>= g)

или

do {                 do {                   do {
   r1 <- do {           x <- m;                r0 <- m;
      r0 <- m;   =      do {            =      r1 <- f r0;
      f r0                 r1 <- f x;          g r1
   };                      g r1             }
   g r1                 }
}                    }

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

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

for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())

while :: Monad m => m Bool -> m t -> m ()
while c m = do
   b <- c
   if b then m >> while c m
        else return ()

forever :: Monad m => m t
forever m = m >> forever m

Ввод, вывод

data World

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

type IO t = World -> (t, World)

Взаимодействие облегчают нечистые примитивы

getChar         :: IO Char
putChar         :: Char -> IO ()
readFile        :: FilePath -> IO String
writeFile       :: FilePath -> String -> IO ()
hSetBuffering   :: Handle -> BufferMode -> IO ()
hTell           :: Handle -> IO Integer
. . .              . . .

Примесь кода, который использует IOпримитивы, постоянно протоколируется системой типов. Потому что чистота потрясающая, что происходит внутри IO, остается внутри IO.

unsafePerformIO :: IO t -> t

Или, по крайней мере, должен.

Подпись типа программы на Haskell

main :: IO ()
main = putStrLn "Hello, World!"

расширяется до

World -> ((), World)

Функция, которая преобразует мир.

эпилог

Категория, объекты которой относятся к типам Haskell, а морфизмы - это функции между типами Haskell - это «быстрая и свободная» категория Hask.

Функтор T- это отображение категории Cв категорию D; для каждого объекта в Cобъекте вD

Tobj :  Obj(C) -> Obj(D)
   f :: *      -> *

и для каждого морфизма в Cморфизме вD

Tmor :  HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
 map :: (a -> b)   -> (f a -> f b)

где X, Yобъекты в C. HomC(X, Y)это класс гомоморфизм всех морфизмов X -> Yв C. Функтор должен сохранять индивидуальность и композицию морфизма, «структуру» C, в D.

                    Tmor    Tobj

      T(id)  =  id        : T(X) -> T(X)   Identity
T(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

Категория Клейла из категории Cдаются Клейли тройки

<T, eta, _*>

эндофунктора

T : C -> C

( f), тождественный морфизм eta( return) и оператор расширения *( =<<).

У каждого Клейсли морфизм в Hask

      f :  X -> T(Y)
      f :: a -> m b

оператором расширения

   (_)* :  Hom(X, T(Y)) -> Hom(T(X), T(Y))
  (=<<) :: (a -> m b)   -> (m a -> m b)

дается морфизм в Haskкатегории Клейсли

     f* :  T(X) -> T(Y)
(f =<<) :: m a  -> m b

Композиция в категории Клейсли .Tприведена в терминах расширения

 f .T g  =  f* . g       :  X -> T(Z)
f <=< g  =  (f =<<) . g  :: a -> m c

и удовлетворяет категории аксиомы

       eta .T g  =  g                :  Y -> T(Z)   Left identity
   return <=< g  =  g                :: b -> m c

       f .T eta  =  f                :  Z -> T(U)   Right identity
   f <=< return  =  f                :: c -> m d

  (f .T g) .T h  =  f .T (g .T h)    :  X -> T(U)   Associativity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d

который, применяя преобразования эквивалентности

     eta .T g  =  g
     eta* . g  =  g               By definition of .T
     eta* . g  =  id . g          forall f.  id . f  =  f
         eta*  =  id              forall f g h.  f . h  =  g . h  ==>  f  =  g

(f .T g) .T h  =  f .T (g .T h)
(f* . g)* . h  =  f* . (g* . h)   By definition of .T
(f* . g)* . h  =  f* . g* . h     . is associative
    (f* . g)*  =  f* . g*         forall f g h.  f . h  =  g . h  ==>  f  =  g

с точки зрения расширения даны канонически

               eta*  =  id                 :  T(X) -> T(X)   Left identity
       (return =<<)  =  id                 :: m t -> m t

           f* . eta  =  f                  :  Z -> T(U)      Right identity
   (f =<<) . return  =  f                  :: c -> m d

          (f* . g)*  =  f* . g*            :  T(X) -> T(Z)   Associativity
(((f =<<) . g) =<<)  =  (f =<<) . (g =<<)  :: m a -> m c

Монады также могут быть определены в терминах не расширения Клейсляна, а естественного преобразования muв вызываемом программировании join. Монада определяется muкак тройка над категорией Cэндофунктора

     T :  C -> C
     f :: * -> *

и две природные трансформации

   eta :  Id -> T
return :: t  -> f t

    mu :  T . T   -> T
  join :: f (f t) -> f t

удовлетворяя эквивалентности

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativity
  join . map join  =  join . join           :: f (f (f t)) -> f t

      mu . T(eta)  =  mu . eta       =  id  :  T -> T               Identity
join . map return  =  join . return  =  id  :: f t -> f t

Класс типа монады затем определяется

class Functor m => Monad m where
   return :: t -> m t
   join   :: m (m t) -> m t

Каноническая muреализация варианта монады:

instance Monad Maybe where
   return = Just

   join (Just m) = m
   join Nothing  = Nothing

concatфункция

concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat []       = []

является joinсписком монады.

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> ([a] -> [b])
   (f =<<) = concat . map f

Реализации joinмогут быть переведены из формы расширения с использованием эквивалентности

     mu  =  id*           :  T . T -> T
   join  =  (id =<<)      :: m (m t) -> m t

Обратный перевод из muформы расширения в форму

     f*  =  mu . T(f)     :  T(X) -> T(Y)
(f =<<)  =  join . map f  :: m a -> m b

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

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

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

от обобщающих монад до стрел Джона Хьюза


4

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

Треугольник Серпинского

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

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


3
Вы имеете в виду "что миру не нужно ..."? Хорошая аналогия, хотя!
Гровербой

@ icc97 ты прав - смысл понятен. Сарказм непреднамеренный, извинения автору.
Гровербой

Мир нуждается в другой ветке комментариев, подтверждающей сарказм, но если внимательно прочитать, я написал, но это должно прояснить это.
Евгений Йокота

4

http://code.google.com/p/monad-tutorial/ находится в стадии разработки, чтобы ответить именно на этот вопрос.



Код Google будет закрыт в 2016-01-15. По состоянию на 2015-08-24 большинство проектов теперь доступны только для чтения.
Питер Мортенсен

4

Позвольте ниже " {| a |m}" представить некоторую часть монадических данных. Тип данных, который рекламирует a:

        (I got an a!)
          /        
    {| a |m}

Функция, fзнает, как создать монаду, если бы она имела a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

Здесь мы видим функцию, fпытается оценить монаду, но получает упрек.

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

Функция, fнаходит способ извлечь aс помощью >>=.

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

Мало ли fзнает, монада и >>=находятся в сговоре.

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

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

Например, тип данных Maybe

 data Maybe a = Nothing | Just a

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

Где, если дело Just a

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

Но для случая Nothing

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

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

Разные монады ведут себя по-разному. Списки - это другие типы данных с монадическими экземплярами. Они ведут себя следующим образом:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

В этом случае функция знала, как составить список из своего ввода, но не знала, что делать с дополнительным вводом и дополнительными списками. Связка >>=, вырученная fпутем объединения нескольких выходов. Я включил этот пример, чтобы показать, что, хотя >>=он отвечает за извлечение a, он также имеет доступ к возможному выходу привязки f. Действительно, он никогда не извлечет ничего, aесли не знает, что конечный результат имеет тот же тип контекста.

Существуют и другие монады, которые используются для представления различных контекстов. Вот некоторые характеристики еще нескольких. IOМонады фактически не имеет a, но он знает парень , и получите , что aдля вас. У State stмонады есть тайник, stкоторый она пропустит fпод столом, хотя fтолько что пришла просить a. Reader rМонада подобна State st, хотя она позволяет только fсмотреть на r.

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

Обратите внимание, что использование >>=облегчает беспорядок, отнимая часть автономии от f. То есть, в приведенном выше случае, Nothingнапример, fбольше не нужно решать, что делать в случае Nothing; это закодировано в >>=. Это компромисс. Если это было необходимо для того, fчтобы решить, что делать в случае Nothing, тогда fдолжна была быть функция от Maybe aдо Maybe b. В этом случае Maybeбыть монадой не имеет значения.

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


3

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

Примером может служить файловый ввод / вывод.

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


3
Как я понимаю, монады - это нечто большее. Инкапсуляция изменяемого состояния в «чистых» функциональных языках - это только одно применение монад.
thSoft
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.