Что такое монада?
Кратко рассмотрев недавно Хаскелла, каким было бы краткое, сжатое, практическое объяснение того, что в сущности является монадой?
Я обнаружил, что большинство объяснений, с которыми я столкнулся, было довольно недоступным и лишенным практических деталей.
50 ответов
Во-первых: термин монада немного бессмыслен, если вы не математик. Альтернативный термин - построитель вычислений, который немного больше описывает то, для чего они действительно полезны.
Вы просите практических примеров:
Пример 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
Монада способна "разделить" блок самостоятельно и отложить выполнение второй половины. (The async {}
синтаксис указывает, что поток управления в блоке определяется async
монада.)
Как они работают
Так как же монада может делать все эти причудливые вещи управления потоком? Что действительно происходит в do-блоке (или вычислительном выражении, как они называются в F#), так это то, что каждая операция (в основном каждая строка) заключена в отдельную анонимную функцию. Эти функции затем объединяются с помощью bind
оператор (пишется >>=
в Хаскеле). Так как 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
Объяснение "что такое монада" немного похоже на выражение "что такое число?" Мы используем номера все время. Но представьте, что вы встретили человека, который ничего не знал о числах. Как, черт возьми, вы объясните, что это за цифры? И как бы вы начали описывать, почему это может быть полезно?
Что такое монада? Короткий ответ: это особый способ объединения операций.
По сути, вы пишете шаги выполнения и связываете их вместе с помощью "функции связывания". (В Хаскеле это называется >>=
.) Вы можете написать вызовы оператору связывания самостоятельно или использовать синтаксический сахар, который заставляет компилятор вставлять эти вызовы функций для вас. Но в любом случае каждый шаг отделяется вызовом этой функции связывания.
Таким образом, функция связывания похожа на точку с запятой; он разделяет шаги в процессе. Задача функции связывания состоит в том, чтобы взять выходные данные с предыдущего шага и передать их на следующий шаг.
Это звучит не слишком сложно, верно? Но есть более чем один вид монады. Зачем? Как?
Ну, функция связывания может просто взять результат с одного шага и передать его на следующий шаг. Но если это "все", что делает монада... это на самом деле не очень полезно. И это важно понимать: каждая полезная монада делает что-то еще, кроме того, что она монада. Каждая полезная монада обладает "особой силой", которая делает ее уникальной.
(Монада, которая не делает ничего особенного, называется "монадой идентичности". Скорее, как функция идентичности, это звучит как совершенно бессмысленная вещь, но оказывается, что это не так… Но это другая история ™.)
По сути, каждая монада имеет свою собственную реализацию функции связывания. И вы можете написать функцию связывания так, чтобы она выполняла определенные действия между этапами выполнения. Например:
Если каждый шаг возвращает индикатор успеха / неудачи, bind может выполнить следующий шаг, только если предыдущий был успешным. Таким образом, неудачный шаг прерывает всю последовательность "автоматически", без каких-либо условных проверок с вашей стороны. (Отказ Монада.)
Расширяя эту идею, вы можете реализовать "исключения". (Монада ошибок или монадаисключений.) Поскольку вы определяете их сами, а не как языковую функцию, вы можете определить, как они работают. (Например, может быть, вы хотите игнорировать первые два исключения и прерывать работу только при возникновениитретьего исключения.)
Вы можете заставить каждый шаг возвращать несколько результатов, и иметь над ними цикл функции привязки, подготавливая каждый следующий шаг к вам. Таким образом, вам не нужно постоянно писать циклы повсеместно при работе с несколькими результатами. Функция связывания "автоматически" сделает все это за вас. (Список Монад.)
Помимо передачи "результата" от одного шага к другому, вы можете использовать функцию связывания, которая также передает дополнительные данные. Эти данные теперь не отображаются в вашем исходном коде, но вы все равно можете получить к ним доступ из любого места, без необходимости вручную передавать их каждой функции. (Читатель Монада.)
Вы можете сделать так, чтобы "дополнительные данные" можно было заменить. Это позволяет вам моделировать деструктивные обновления, фактически не делая деструктивных обновлений. (Государственная Монада и ее двоюродный братПисатель Монада.)
Поскольку вы имитируете только деструктивные обновления, вы можете тривиально делать то, что было бы невозможно с реальными деструктивными обновлениями. Например, вы можете отменить последнее обновление или вернуться к более старой версии.
Вы можете создать монаду, в которой вычисления могут быть приостановлены, так что вы можете приостановить свою программу, войти и поработать с внутренними данными состояния, а затем возобновить их.
Вы можете реализовать "продолжения" как монаду. Это позволяет сломать умы людей!
Все это и многое другое возможно с монадами. Конечно, все это также вполне возможно и без монад. С помощью монад просто проще.
На самом деле, вопреки общему пониманию монад, они не имеют ничего общего с государством. Монады - это просто способ обернуть вещи и предоставить методы для выполнения операций над обернутыми вещами, не разворачивая их.
Например, вы можете создать тип для переноса другого в 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.
Что касается понимания монад, не беспокойтесь об этом. Читайте о них то, что вам интересно, и не беспокойтесь, если не поймете сразу. Тогда просто дайвинг на таком языке, как Хаскелл, - путь. Монады - это одна из тех вещей, где понимание проникает в ваш мозг практикой, однажды вы вдруг понимаете, что понимаете их.
Но вы могли бы изобрести монады!
Sigfpe говорит:
Но все они представляют монады как нечто эзотерическое, нуждающееся в объяснении. Но я хочу сказать, что они вовсе не эзотерические. Фактически, столкнувшись с различными проблемами в функциональном программировании, вы неизбежно привели бы к определенным решениям, которые все являются примерами монад. На самом деле, я надеюсь заставить вас изобретать их сейчас, если вы этого еще не сделали. Тогда это маленький шаг, чтобы заметить, что все эти решения на самом деле являются одним и тем же замаскированным решением. И после прочтения этого вы можете лучше понять другие документы по монадам, потому что вы узнаете все, что видите, как то, что вы уже изобрели.
Многие проблемы, которые пытаются решить монады, связаны с проблемой побочных эффектов. Итак, начнем с них. (Обратите внимание, что монады позволяют вам делать больше, чем просто справляться с побочными эффектами, в частности, многие типы контейнерных объектов можно рассматривать как монады. В некоторых введениях в монады трудно совместить эти два различных применения монад и сосредоточиться только на одной или другой.)
В императивном языке программирования, таком как C++, функции ведут себя не так, как функции математики. Например, предположим, что у нас есть функция C++, которая принимает один аргумент с плавающей запятой и возвращает результат с плавающей запятой. Внешне это может показаться немного похожим на математическую функцию, отображающую реальное в реальное, но функция C++ может сделать больше, чем просто вернуть число, которое зависит от ее аргументов. Он может читать и записывать значения глобальных переменных, а также записывать вывод на экран и получать ввод от пользователя. Однако на чистом функциональном языке функция может читать только то, что ей предоставлено в своих аргументах, и единственный способ, которым она может влиять на мир, - это возвращаемые значения.
Монада - это тип данных, который имеет две операции: >>=
(ака 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, чтобы оценить, что такое монады и почему они полезны.
Сначала вы должны понять, что такое функтор. Перед этим разберитесь с функциями высшего порядка.
Функция высшего порядка - это просто функция, которая принимает функцию в качестве аргумента.
Функтор - конструкция любого типа 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
,
[Отказ от ответственности: я все еще пытаюсь полностью поглощать монады. Вот что я понял до сих пор. Если это не так, надеюсь, кто-то знающий позвонит мне на ковер.]
Арнар написал:
Монады - это просто способ обернуть вещи и предоставить методы для выполнения операций над обернутыми вещами, не разворачивая их.
Это именно так. Идея звучит так:
Вы берете какую-то ценность и оборачиваете ее дополнительной информацией. Точно так же, как значение определенного типа (например, целое число или строка), так и дополнительная информация имеет определенный вид.
Например, эта дополнительная информация может быть
Maybe
илиIO
,Затем у вас есть несколько операторов, которые позволяют вам работать с обернутыми данными, сохраняя при этом эту дополнительную информацию. Эти операторы используют дополнительную информацию, чтобы решить, как изменить поведение операции для переносимого значения.
Например,
Maybe Int
может бытьJust Int
или жеNothing
, Теперь, если вы добавитеMaybe Int
кMaybe Int
, оператор проверит, оба ли ониJust Int
S внутри, и если так, будет развернутьInt
s, передайте им оператор сложения, заново оберните полученный результатInt
в новыйJust Int
(который является действительнымMaybe Int
) и, таким образом, вернутьMaybe Int
, Но если один из них былNothing
внутри этот оператор просто сразу вернетсяNothing
что опять-таки является действительнымMaybe Int
, Таким образом, вы можете делать вид, что вашMaybe Int
Они просто нормальные числа и выполняют обычные математические операции с ними. Если бы вы должны были получитьNothing
, ваши уравнения будут по-прежнему давать правильный результат - без необходимости мусорить проверки дляNothing
везде
Но пример только то, что происходит для Maybe
, Если дополнительная информация была IO
тогда этот специальный оператор, определенный для IO
Вместо этого будет вызван s, и он может сделать что-то совершенно другое перед выполнением сложения. (ОК, добавив два IO Int
s вместе, вероятно, бессмысленно - я еще не уверен.) (Кроме того, если вы обратили внимание на Maybe
Например, вы заметили, что "оборачивать значение дополнительными вещами" не всегда правильно. Но трудно быть точным, правильным и точным, не будучи непостижимым.)
По сути, "монада" грубо означает "шаблон". Но вместо книги, полной неформально объясненных и конкретно названных шаблонов, у вас теперь есть языковая конструкция - синтаксис и все - что позволяет вам объявлять новые шаблоны как вещи в вашей программе. (Неточность здесь заключается в том, что все паттерны должны следовать определенной форме, поэтому монада не так универсальна, как паттерн. Но я думаю, что это самый близкий термин, который большинство людей знают и понимают.)
И именно поэтому люди находят монады настолько запутанными: потому что у них такая общая концепция. Спрашивать, что делает что-то монадой, также неопределенно, как спрашивать, что делает что-то образцом.
Но подумайте о последствиях наличия синтаксической поддержки в языке для идеи шаблона: вместо того, чтобы читать книгу " Банда четырех" и запоминать конструкцию определенного шаблона, вы просто пишете код, который реализует этот шаблон в агностике, общий способ один раз, и тогда вы сделали! Затем вы можете повторно использовать этот шаблон, например Visitor, Strategy, Façade или любой другой, просто украсив им операции в своем коде, без необходимости повторной реализации его снова и снова!
Вот почему люди, которые понимают монады, находят их такими полезными: это не какая-то концепция башни из слоновой кости, которую интеллектуальные снобы гордятся пониманием (хорошо, это тоже, конечно, хихикать), но на самом деле делает код проще.
После долгих попыток, я думаю, я наконец понял монаду. Перечитав мою собственную длинную критику ответа с подавляющим большинством голосов, я предложу это объяснение.
Есть три вопроса, на которые нужно ответить, чтобы понять монады:
- Зачем тебе нужна монада?
- Что такое монада?
- Как реализована монада?
Как я отмечал в моих первоначальных комментариях, слишком много объяснений монады попадают в вопрос № 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
оператор, который превращает нормальные функции, в функции, которые работают с этим специфическим видом оператора связывания.
Как реализована монада?
Посмотрите другие ответы, которые, кажется, совершенно свободно прыгать в детали этого.
Я написал это в основном для меня, но я надеюсь, что другие найдут это полезным:)
Я считаю это объяснение более правильным. Тем не менее, я думаю, что это лечение по-прежнему ценно и рассмотрит его включение позже. Достаточно сказать, что там, где обычная композиция функций имеет дело с простыми значениями функций, монады предназначены для составления функций, которые оперируют значениями функций (функциями более высокого порядка). Когда вы имеете дело с функциями более высокого порядка (функциями, которые принимают или возвращают функции), композиция должна быть настроена или адаптирована так, чтобы оценивать операнды при оценке композиции. Этот процесс оценки может быть экзотическим, например, сбор результатов асинхронных процессов. Тем не менее, этот пошив может быть выполнен по образцу. Версия этого паттерна называется монадой и очень сильно следует алгебраическому сложению. В частности, в отношении следующего содержимого такие функции более высокого порядка будут рассматриваться как математические операторы в выражении, принимающие в качестве операндов другие частично примененные операторы, и, таким образом, функции 1+ 2*, 3/ и 7+ в 1+ ( 2* ( 3/ ( 7+ (..) ) ) )
...
Монады решают проблему, которая также отображается в арифметике как деление на ноль, DivByZero
, В частности, расчеты с участием деления должны обнаруживать или учитывать DivByZero
исключение. Это требование затрудняет кодирование таких выражений в общем случае.
Монадическое решение - принять DivByZero
сделав следующее
- Разверните
Number
тип для включенияDivByZero
как конкретное значение, которое не является обычным числом:NaN
,Infinity
, или жеNull
, Давайте назовем этот новый тип номера,Nullable<Number>
, - Предоставить функцию для "поднятия" или обертывания существующего
Number
вNullable<Number>
тип (идея "упаковки" заключается в том, что содержаниеNumber
или значение может быть "развернуто" без потери информации) - Предоставить функцию для "подъема" или переноса существующих операторов на
Number
в версии, которая работает наNullable<Number>
, Такой результирующий "поднятый" оператор может просто сделать следующее:- развернуть при условии
Nullable<Number>
операнды и применять его содержащиесяNumber
оператор на них потом "поднимет" получившийсяNumber
результат вNullable<Number>
- обнаружить
DivByZero
операнд или исключение во время оценки и путем дальнейшей оценки, производяDivByZero
значение как результат, чтобы утверждать, что (1 + Null = Null
). Однако, какие действия предпринять, зависит от программиста. В общем, эти функции-обертки - это то, где написано много функциональных возможностей монад. Информация о монадическом состоянии поддерживается в самом типе оболочки, откуда проверяются упакованные функции и, в соответствии с подходом неизменяемости функционального программирования, создают новое монадическое значение. В случаеNullable<Number>
такая монадическая информация о состоянии будет описыватьDivByZero
или фактическийNumber
существует.
- развернуть при условии
Итак, Monad - это расширенный тип вместе с функцией, которая "оборачивает" исходный тип в эту расширенную версию, и другой функцией, которая оборачивает исходные операторы, чтобы они могли обрабатывать этот новый расширенный тип. (Монады, возможно, были мотивацией для обобщений или параметров типа.)
Оказывается, вместо того, чтобы просто сгладить обработку DivByZero
(или Infinity, если хотите), обработка Monad широко применима к ситуациям, которые могут выиграть от расширения типов для упрощения их кодирования. На самом деле эта применимость кажется широкой.
Например, IO Monad
это тип, который представляет вселенную, в буквальном смысле. Цель состоит в том, чтобы признать, что значения, возвращаемые прототипом HelloWorld
Программа не полностью описывается типом результата string
и его значение "Hello World!". Фактически, такой результат также включает в себя модификации аппаратных средств и состояний памяти таких устройств, как консоль. Например, после выполнения консоль теперь отображает дополнительный текст, курсор находится на новой строке и т. Д. IO Monad
это просто явное признание таких внешних эффектов или побочных эффектов, если хотите.
Зачем беспокоиться?
Монады позволяют разрабатывать алгоритмы без сохранения состояния и документировать алгоритмы с полным состоянием. Полноценные машины сложны. Например, машина с 10 битами может находиться в 2^10 возможных состояниях. Устранение лишней сложности - идеал функциональных языков.
Переменные содержат состояние. Устранение "переменных" должно просто напичкать. Чисто функциональные программы не обрабатывают переменные, а только значения (несмотря на использование термина "переменная" в документации по Haskell) и вместо этого используют метки, символы или имена для таких значений по мере необходимости. Следовательно, самая близкая вещь к переменной в чисто функциональном языке - это параметры, полученные функцией, так как они принимают новые значения при каждом вызове. (Метка относится к значению, тогда как переменная относится к месту, в котором хранится значение. Следовательно, вы можете изменить содержимое переменной, но метка является самим содержимым. В конечном итоге лучше дать яблоко, чем мешок с яблоком, возможно, в нем.)
Отсутствие переменных объясняет, почему чисто функциональные языки используют рекурсию вместо циклов для итерации. Инкремент счетчика включает в себя использование переменной, которая становится инкрементной, и всю неопределенность с тем, как она обновляется, когда она тестируется, какое значение она должна иметь и когда, а затем сложность, когда у вас есть несколько потоков, потенциально обращающихся к этому. та же переменная.
Тем не менее, и что?
Без присутствия состояния функция должна стать декларацией или определением ее результатов, в отличие от зачисления некоторого базового состояния в сторону результата. По сути, функциональное выражение incFun(x) = x + 1
проще, чем императивное выражение incImp(x) = x.add(1); return x;
Вот, incFun
не модифицирует x
но создает новую ценность. incFun может даже быть заменен его определением в выражениях, как в 1 + incFun(x)
становление 1 + (x + 1)
, С другой стороны, incImp
изменяет состояние x
, Что бы ни означало такое изменение для x
может быть неясным и в конечном итоге может быть невозможно определить без выполнения программы в дополнение к каким-либо проблемам параллелизма.
Такая сложность становится когнитивно дорогой со временем (2^N). В отличие от оператора, +
не может изменить x
но вместо этого необходимо создать новое значение, результат которого ограничен и полностью определяется значениями x
а также 1
и определение +
, В частности, предотвращается взрыв сложности 2 ^ N. Кроме того, чтобы подчеркнуть параллелизм, incImp
, В отличие от incFun
, не может быть вызван одновременно без мер предосторожности при совместном использовании параметра, так как он становится модифицированным при каждом вызове.
Зачем называть это монадой?
Монада характеризуется математической структурой, называемой моноидом из теории алгебраических групп. С учетом сказанного все это означает, что моноид имеет следующие три свойства:
- имеет бинарный оператор,
*
такой, чтоx * y = z
заx, y, and z
принадлежность к какому-то типуS
, Например, 1 ÷ 2 = 0,5, где 1, 2 и 0,5 относятся к типуNumber
, Закрыто - имеет элемент идентичности,
i
, связанный с бинарным оператором, который не делает ничего такого, что(i * x) = (x * i) = x
, Например, числовой оператор + и число 0 в 4 + 0 = 0 + 4 = 4. Идентичность - Порядок оценки "сегментов" не имеет значения:
(x * y) * z = x * (y * z)
, Например, числовой оператор +, в(3 + 4) + 12 = 3 + (4 + 12) = 19
, Обратите внимание, однако, что последовательность терминов не должна меняться. Ассоциативность
Свойство три (Ассоциативность) позволяет оценивать выражения произвольной длины, разделяя их на сегменты и оценивая каждый сегмент независимо, например, параллельно. Например, x1*x2*...*xN
может быть разделен на (x1..xJ) * (xJ+1...xM) * (xM+1...xN)
, Отдельный результат, x0J * xJM * xMN
Затем могут быть собраны и в дальнейшем оценены аналогичным образом. Подобная поддержка сегментации является ключевой техникой, обеспечивающей правильный параллелизм и распределенную оценку в соответствии с алгоритмами распределенного поиска Google (а-ля карта / уменьшение).
Свойство два (Идентичность) позволяет упростить построение выражений различными способами, хотя это может быть не совсем очевидно; однако, точно так же, как ноль, очевидно, не был необходим для ранних систем подсчета, он полезен как концепция пустого, так и для переноса пустого значения. Обратите внимание, что в типе, Nullable<Number>
, Null
это не пустое значение, а скорее DivByZero
, В частности, nn + DivByZero = DivByZero
в то время как nn + 0 = 0 + nn = nn
отсюда 0
остается личность под +
, где nn
любой Nullable<Number>
,
Наконец, есть причина, по которой мы больше не используем римские цифры... нет расширенного приспособления для нуля или дробей, иррациональных чисел, отрицательных чисел, мнимых чисел,... да, похоже, наша система счисления может считаться монадой.
Монада, по сути, является формой "оператора типа". Это сделает три вещи. Сначала он "обернет" (или иным образом преобразует) значение одного типа в другой тип (обычно называемый "монадическим типом"). Во-вторых, он сделает все операции (или функции) доступными для базового типа доступными для монадического типа. Наконец, он обеспечит поддержку для объединения себя с другой монадой для создания составной монады.
"Возможно, монада" по существу эквивалентна "обнуляемым типам" в Visual Basic / C#. Он принимает необнуляемый тип "T" и преобразует его в "Nullable
Побочные эффекты представлены одинаково. A structure is created that holds descriptions of side effects alongside a function's return value. The "lifted" operations then copy around side effects as values are passed between functions.
They are called "monads" rather than the easier-to-grasp name of "type operators" for several reasons:
- Monads have restrictions on what they can do (see the definiton for details).
- Those restrictions, along with the fact that there are three operations involved, conform to the structure of something called a monad in Category Theory, which is an obscure branch of mathematics.
- They were designed by proponents of "pure" functional languages
- Proponents of pure functional languages like obscure branches of mathematics
- Because the math is obscure, and monads are associated with particular styles of programming, people tend to use the word monad as a sort of secret handshake. Because of this no one has bothered to invest in a better name.
(См. Также ответы в разделе Что такое монада?)
Хорошей мотивацией для монад является sigfpe (Дэн Пипони), " Вы могли бы изобрести монады" ! (А может быть, у вас уже есть). Существует множество других учебных пособий по монадам, многие из которых ошибочно пытаются объяснить монады "простыми терминами", используя различные аналогии: это ошибка учебного пособия по монадам; избежать их.
Как говорит DR MacIver в Расскажите нам, почему ваш язык отстой:
Итак, вещи, которые я ненавижу в Haskell:
Начнем с очевидного. Монады учебники. Нет, не монады. В частности, учебники. Они бесконечные, раздутые и дорогой бог, они утомительны. Кроме того, я никогда не видел убедительных доказательств того, что они действительно помогают. Прочитайте определение класса, напишите некоторый код, переберите страшное имя.
Вы говорите, что понимаете монаду Может быть? Хорошо, ты уже в пути. Просто начните использовать другие монады, и рано или поздно вы поймете, что такое монады в целом.
[Если вы ориентированы математически, вы можете игнорировать десятки учебных пособий и выучить определение или следовать лекциям по теории категорий:) Основная часть определения состоит в том, что Monad M включает в себя "конструктор типов", который определяет для каждого существующий тип "T", новый тип "M T" и некоторые способы перехода между "обычными" типами и "M".]
Кроме того, как ни удивительно, одним из лучших введений в монады на самом деле является одна из ранних научных статей, представляющих монады, монады Филиппа Уодлера для функционального программирования. На самом деле в ней есть практические, нетривиальные мотивирующие примеры, в отличие от многих искусственных учебников.
Монады должны управлять потоком, каким абстрактные типы данных являются для данных.
Другими словами, многим разработчикам нравится идея наборов, списков, словарей (или хэшей, или карт) и деревьев. Внутри этих типов данных существует много особых случаев (например, InsertionOrderPreservingIdentityHashMap).
Однако, сталкиваясь с программным потоком, многие разработчики не сталкивались с гораздо большим количеством конструкций, чем if, switch/case, do, while, goto (grr) и (возможно) замыкания.
Итак, монада - это просто конструкция потока управления. Лучшей фразой для замены монады будет "тип управления".
Таким образом, монада имеет слоты для управляющей логики, или операторов, или функций - эквивалент в структурах данных будет означать, что некоторые структуры данных позволяют вам добавлять данные и удалять их.
Например, монада "если":
if( clause ) then block
в самом простом случае имеет два слота - предложение и блок. if
Монада обычно строится для оценки результата предложения, и, если не ложь, оценивает блок. Многие разработчики не знакомы с монадами, когда они изучают "если", и просто нет необходимости понимать монады, чтобы писать эффективную логику.
Монады могут стать более сложными, точно так же, как структуры данных могут стать более сложными, но есть много широких категорий монад, которые могут иметь сходную семантику, но отличающиеся реализации и синтаксис.
Конечно, таким же образом, что структуры данных могут быть перебраны или пройдены по монадам, могут быть оценены.
Компиляторы могут иметь или не иметь поддержку пользовательских монад. Хаскелл, конечно, делает. Ioke имеет некоторые аналогичные возможности, хотя термин монада не используется в языке.
Мой любимый учебник Monad:
http://www.haskell.org/haskellwiki/All_About_Monads
(из 170000 просмотров в поиске Google "учебник монады"!)
@Stu: Смысл монад в том, чтобы позволить вам добавить (обычно) последовательную семантику в чистый код; Вы даже можете создавать монады (используя Monad Transformers) и получать более интересную и сложную комбинированную семантику, например, например, анализ с обработкой ошибок, общим состоянием и журналированием. Все это возможно в чистом коде, монады просто позволяют абстрагировать его и повторно использовать в модульных библиотеках (всегда хороших в программировании), а также предоставляют удобный синтаксис, чтобы он выглядел обязательным.
На Haskell уже есть перегрузка операторов [1]: он использует классы типов во многом так же, как можно использовать интерфейсы в Java или C#, но в Haskell просто допускается использование не алфавитно-цифровых токенов, таких как + && и>, в качестве идентификаторов инфиксов. Это только перегрузка оператора, если вы имеете в виду "перегрузка точки с запятой" [2]. Это звучит как чёрная магия, и возникает проблема с "перегрузкой точки с запятой" (представьте, что предприимчивые хакеры Perl узнают об этой идее), но дело в том, что без монад нет точки с запятой, поскольку чисто функциональный код не требует или не допускает явной последовательности.
Все это звучит намного сложнее, чем нужно. Статья sigfpe довольно крутая, но для объяснения она использует Haskell, что не решает проблему курицы и яйца, заключающуюся в том, чтобы понять Haskell, чтобы прогнать Monads, и понять Monads, чтобы проглотить Haskell.
[1] Это отдельная проблема от монад, но монады используют функцию перегрузки операторов Haskell.
[2] Это также упрощение, так как оператор для цепочки монадических действий - >>= (произносится как "связать"), но есть синтаксический сахар ("до"), который позволяет вам использовать фигурные скобки и точки с запятой и / или отступы и символы новой строки.
Я все еще новичок в монадах, но я подумал, что поделюсь ссылкой, которую я нашел, и которую очень приятно читать (С ФОТОГРАФИЯМИ!): http://www.matusiak.eu/numerodix/blog/2012/3/11/monads-for-the-layman/(без принадлежности)
По сути, теплая и нечеткая концепция, которую я получил из этой статьи, заключалась в том, что монады - это в основном адаптеры, которые позволяют разнородным функциям работать комбинируемым образом, т.е. иметь возможность объединять несколько функций, смешивать и сопоставлять их, не беспокоясь о непоследовательном возврате. типы и тому подобное. Таким образом, функция BIND отвечает за хранение яблок с яблоками и апельсинов с апельсинами, когда мы пытаемся сделать эти адаптеры. А функция LIFT отвечает за использование функций "более низкого уровня" и "модернизацию" их для работы с функциями BIND, а также для их компоновки.
Надеюсь, я понял правильно, и, что более важно, надеюсь, что статья имеет правильное представление о монадах. Эта статья помогла мне разжечь аппетит к изучению монад.
ТЛ; др
{-# 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
, Расширение или приложение Клейсли =<<
применяет стрелку Клейсли 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.
return <=< g = g :: b -> m c Left identity
f <=< return = f :: c -> m d Right 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 использует свой флип, оператор связывания >>=
,
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
fail :: String -> m a
fail s = errorWithoutStackTrace s
Для простоты в этом объяснении используется иерархия классов типов
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) >>
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.
fail <|> l = l
k <|> fail = k
(k <|> l) <|> m = k <|> (l <|> m)
а также fail
является поглощающим / уничтожающим нулевым элементом аддитивных монад
_ =<< fail = fail
Если в
guard (even p) >> return p
even p
верно, то охранник выдает [()]
и, по определению >>
, функция локальной константы
\ _ -> return p
применяется к результату ()
, Если ложь, то охранник создает список монады fail
[]
, который не дает результата для применяемой стрелки Клейсли >>
к.
государственный
Печально, монады используются для кодирования вычислений с состоянием.
Состояние процессора является функцией
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
в С.
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
Филипп Вадлер: монады для функционального программирования
Саймон Л. Пейтон Джонс, Филип Уодлер: императивное функциональное программирование
Джонатан М.Д. Хилл, Кит Кларк: введение в теорию категорий, монады теории категорий и их связь с функциональным программированием
Эудженио Могги: понятия вычисления и монады
Но почему такая абстрактная теория должна быть полезна для программирования?
Ответ прост: как компьютерные ученые, мы ценим абстракцию! Когда мы разрабатываем интерфейс к программному компоненту, мы хотим, чтобы он как можно меньше раскрывал реализацию. Мы хотим иметь возможность заменить реализацию многими альтернативами, многими другими "экземплярами" той же "концепции". Когда мы разрабатываем общий интерфейс для многих программных библиотек, еще более важно, чтобы у выбранного нами интерфейса было множество реализаций. Мы высоко ценим общность концепции монады, потому что теория категорий настолько абстрактна, что ее концепции так полезны для программирования.
Поэтому вряд ли удивительно, что обобщение монад, которое мы представляем ниже, также имеет тесную связь с теорией категорий. Но мы подчеркиваем, что наша цель очень практичная: это не "реализация теории категорий", а поиск более общего способа структурирования библиотек комбинаторов. Нам просто повезло, что математики уже проделали большую часть работы за нас!
от обобщающих монад до стрел Джон Хьюз
В последнее время я думаю о Монаде иначе. Я думал о них как об абстрактном порядке выполнения математическим способом, который делает возможными новые виды полиморфизма.
Если вы используете императивный язык и пишете некоторые выражения по порядку, код ВСЕГДА выполняется именно в этом порядке.
И в простом случае, когда вы используете монаду, вы чувствуете то же самое - вы определяете список выражений, которые встречаются по порядку. Кроме того, в зависимости от того, какую монаду вы используете, ваш код может выполняться по порядку (как в монаде IO), параллельно по нескольким элементам одновременно (как в монаде списка), он может останавливаться на полпути (как в монаде Maybe), он может приостановиться на полпути, чтобы возобновиться позже (как в монаде Возобновления), может перемотаться и начаться с начала (как в монаде Транзакции), или он может перемотаться на полпути, чтобы попробовать другие варианты (как в монаде Логики),
А поскольку монады полиморфны, можно запускать один и тот же код в разных монадах в зависимости от ваших потребностей.
Кроме того, в некоторых случаях возможно объединение монад (вместе с преобразователями монад) для одновременного получения нескольких функций.
Монады - это не метафоры, а практически полезная абстракция, возникающая из общего паттерна, как объясняет Даниэль Спивак.
В дополнение к превосходным ответам, приведенным выше, позвольте мне предложить вам ссылку на следующую статью (Патрик Томсон), в которой объясняются монады, связывая концепцию с библиотекой JavaScript jQuery (и ее способ использования "цепочки методов" для манипулирования DOM).: jQuery - это монада
Сама документация по jQuery не относится к термину "монада", но говорит о "шаблоне компоновщика", который, вероятно, более знаком. Это не меняет того факта, что у вас есть настоящая монада, может быть, даже не осознавая этого.
На практике monad - это пользовательская реализация оператора композиции функций, которая заботится о побочных эффектах и несовместимых входных и возвращаемых значениях (для формирования цепочки).
Монада - это способ объединения вычислений в общий контекст. Это похоже на построение сети труб. При построении сети данные по ней не передаются. Но когда я закончил объединять все биты вместе с "bind" и "return", я вызываю что-то вроде runMyMonad monad data
и данные текут по каналам.
Monad
является Applicative
(т.е. что-то, что вы можете поднять функции любой арности) Functor
(то есть что-то, что вы можете отобразить) с добавленной способностью выравнивать тип данных. В Haskell эта операция выравнивания называется join
,
Для списков конкретный тип:
join :: [[a]] -> [a]
Общий тип, который работает для любой монады:
join :: Monad m => m (m a) -> m a
Привязка (>>=
) оператор это просто комбинация fmap
а также join
т.е. m >>= f = join (fmap f m)
,
Для многих монад, оба do-обозначения x <- m
и оператор связывания может быть прочитан как
сначала сделайте m, и когда это будет сделано, сохраните результат в x и позвольте мне использовать это, чтобы сделать что-то еще.
Этот ответ начинается с мотивирующего примера, проходит через пример, выводит пример монады и формально определяет "монаду".
Рассмотрим эти три функции в псевдокоде:
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 _
относится к типу после заполнения пробела с типом. M int
это пара из int и строки. 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 >>=
в Хаскеле) и wrap
называется return
,
Monoid, по-видимому, гарантирует, что все операции, определенные для Monoid и поддерживаемого типа, всегда будут возвращать поддерживаемый тип внутри Monoid. Например, любое число + любое число = число, без ошибок.
Принимая во внимание, что деление принимает два дробных числа и возвращает дробное число, которое определило деление на ноль как бесконечность в haskell, почему-то (что иногда является дробным некоторым образом)...
В любом случае кажется, что Monads - это просто способ гарантировать, что ваша цепочка операций ведет себя предсказуемо, а функция, которая претендует на значение Num -> Num, составленная с другой функцией Num-> Num, вызываемой с помощью x, не скажем, стрелять ракетами.
С другой стороны, если у нас есть функция, которая запускает ракеты, мы можем объединить ее с другими функциями, которые также запускают ракеты, потому что наше намерение ясно - мы хотим запустить ракеты - но она не будет пытаться печать "Hello World" по какой-то странной причине.
В Haskell main имеет тип IO () или IO [()], это странное распределение, и я не буду его обсуждать, но вот что я думаю:
Если у меня есть main, я хочу, чтобы он выполнял цепочку действий, потому что я запускаю программу, чтобы произвести эффект - обычно через IO. Таким образом, я могу объединить операции ввода-вывода в главном порядке, чтобы выполнить ввод-вывод, ничего больше.
Если я пытаюсь сделать что-то, что не "возвращает IO", программа будет жаловаться на то, что цепочка не течет, или, по сути, "как это связано с тем, что мы пытаемся сделать - действие IO", это, кажется, заставляет программист, чтобы держать их ход мыслей, не отклоняясь и не думая об увольнении ракет, создавая алгоритмы для сортировки - что не происходит.
По сути, Monads, кажется, подсказка компилятору: "эй, вы знаете эту функцию, которая возвращает число здесь, она на самом деле не всегда работает, иногда она может генерировать число, а иногда вообще ничего, просто держите это в разум". Зная это, если вы пытаетесь утвердить монадическое действие, монадическое действие может действовать как исключение времени компиляции, говоря "эй, это на самом деле не число, это МОЖЕТ быть числом, но вы не можете этого предположить, что-то сделать чтобы гарантировать, что поток является приемлемым." что предотвращает непредсказуемое поведение программы - в значительной степени.
Похоже, монады не о чистоте и не контроле, а о сохранении идентичности категории, в которой все поведение предсказуемо и определено или не компилируется. Вы не можете ничего делать, когда от вас ожидают что-то сделать, и вы не можете делать что-то, если от вас ожидают ничего (видимого).
Самая большая причина, по которой я мог придумать Monads, - это посмотреть на процедурный / ООП-код, и вы заметите, что вы не знаете, где начинается или заканчивается программа, все, что вы видите, это много прыжков и много математики, магия и ракеты. Вы не сможете поддерживать его, и, если сможете, вы потратите довольно много времени, чтобы сосредоточиться на всей программе, прежде чем сможете понять какую-либо ее часть, потому что модульность в этом контексте основана на взаимозависимых "разделах". кода, где код оптимизирован таким образом, чтобы быть как можно более связанными для обеспечения эффективности / взаимосвязи. Монады очень конкретны и хорошо определены по определению и обеспечивают возможность анализа потока программы и выделения частей, которые трудно анализировать - так как они сами являются монадами. Монада представляется "понятной единицей, которая предсказуема при ее полном понимании". Если вы понимаете монаду "Возможно", то нет никакого способа, которым она будет делать что-либо, кроме как "Может быть", что кажется тривиальным, но в большинстве не монадических код, простая функция "helloworld" может запускать ракеты, ничего не делать, или разрушать вселенную, или даже искажать время - мы не имеем ни малейшего представления о том, ЧТО ЭТО ТАКОЕ. Монада ГАРАНТИРУЕТ, ЧТО ЭТО ТАКОЕ. что очень сильно.
Все вещи в "реальном мире" кажутся монадами в том смысле, что они связаны определенными наблюдаемыми законами, предотвращающими путаницу. Это не означает, что мы должны имитировать все операции этого объекта для создания классов, вместо этого мы можем просто сказать "квадрат есть квадрат", ничего, кроме квадрата, даже не прямоугольника и не круга, и "квадрат имеет площадь длины одного из существующих измерений, умноженных на себя. Независимо от того, какой у вас квадрат, если это квадрат в 2D-пространстве, его площадь абсолютно не может быть ничем, кроме его длины в квадрате, это почти тривиально доказать. Это очень сильно, потому что нам не нужно делать утверждения, чтобы убедиться, что наш мир такой, какой он есть, мы просто используем значение реальности, чтобы не допустить срыва наших программ.
Я гарантированно ошибаюсь, но я думаю, что это может кому-то помочь, так что, надеюсь, это кому-то поможет.
Монада — это контейнер, но для данных. Специальный контейнер.
Все контейнеры могут иметь отверстия, ручки и носики, но все эти контейнеры гарантированно имеют определенные отверстия, ручки и носики.
Почему? Потому что эти гарантированные отверстия, ручки и носики полезны для захвата и соединения контейнеров определенными, обычными способами.
Это позволяет вам подбирать различные контейнеры, не зная о них многого. Это также позволяет легко соединять различные типы контейнеров.
Я постараюсь объяснить 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
Тип класса определяет >>=
оператор, который позволяет нам передавать значения в контексте (мы называем эти монадические значения) функциям, которые не ожидают значений в контексте. Контекст будет заботиться о.
Если здесь есть что вспомнить, то это Monad
s позволяет составлять функции, которые включают значения в контекстах.
Две вещи, которые помогли мне лучше всего, узнав об этом, были:
Глава 8, "Функциональные парсеры", из книги Грэма Хаттона " Программирование на Хаскеле". На самом деле это вообще не касается монад, но если вы сможете проработать главу и по-настоящему понять все в ней, особенно то, как оценивается последовательность операций связывания, вы поймете внутренности монад. Ожидайте, что это займет несколько попыток.
Учебник Все о монадах. Это дает несколько хороших примеров их использования, и я должен сказать, что аналогия в Appendex я работал для меня.
Если я правильно понял, IEnumerable является производным от монад. Интересно, может ли это быть интересным углом зрения для тех из нас, кто живет в мире C#?
Для чего это стоит, вот несколько ссылок на учебники, которые мне помогли (и нет, я до сих пор не понял, что такое монады).
В контексте 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
http://code.google.com/p/monad-tutorial/ находится в стадии разработки, чтобы ответить именно на этот вопрос.
Очень простой ответ:
Монады - это абстракция, которая обеспечивает интерфейс для инкапсуляции значений, для вычисления новых инкапсулированных значений и для разворачивания инкапсулированного значения.
Что на практике удобно для них, так это то, что они предоставляют единый интерфейс для создания типов данных, которые моделируют состояние, но не сохраняют состояние.
Важно понимать, что Monad - это абстракция, то есть абстрактный интерфейс для работы с определенным типом структуры данных. Этот интерфейс затем используется для создания типов данных, которые имеют монадическое поведение.
Вы можете найти очень хорошее и практичное введение в Monads в Ruby, часть 1: Введение.