Первый: термин монада немного пустоват, если вы не математик. Альтернативный термин - построитель вычислений , который немного больше описывает то, для чего они на самом деле полезны.
Вы запрашиваете практические примеры:
Пример 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
операции объединяются в цепочку так, что они выполняются последовательно, за исключением случаев, когда выдается ошибка, и в этом случае оставшаяся часть цепочки отбрасывается.
И синтаксис со списком, и с нотацией являются синтаксическим сахаром для операций цепочки с использованием оператора >>=
. Монада - это просто тип, который поддерживает оператор >>=
.
Пример 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 # также поддерживает монады. Этот пример украден у Don Syme :
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