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

на мой скромный взгляд ответы на знаменитый вопрос

7 ответов


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

  1. мы хотим, чтобы программа только с помощью функции. ("функциональное программирование (FP)" в конце концов).
  2. тогда у нас есть первая большая проблема. Это программа:

    f(x) = 2 * x

    g(x,y) = x / y

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

    устранение: compose функции. Если вы хотите сначала g а то f, просто писать f(g(x,y)). Таким образом, "программа" также является функцией: main = f(g(x,y)). Хорошо, но .....

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

    решение: Давайте разрешить функциям возвращать два вида вещей вместо g : Real,Real -> Real (функция из двух Реалов в реальный), давайте позволим g : Real,Real -> Real | Nothing (функция от двух Реалов в (настоящий или нет)).

  4. но функции должны (быть проще) возвратить только одно.

    решение: давайте создадим новый тип данных для возврата,"бокс типаg : Real,Real -> Maybe Real. Хорошо, но .....

  5. что теперь будет с f(g(x,y))? f не готов потреблять Maybe Real. И мы не хотим менять каждую функцию, с которой мы могли бы связаться с g использовать Maybe Real.

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

    в нашем случае: g >>= f (connect/compose g to f). Мы хотим >>= и g вывод, проверьте его и, в случае, если это Nothing просто не звоните f и возврат Nothing; или, наоборот, извлечь в коробке Real и кормить f С ним. (Этот алгоритм является просто реализацией >>= на Maybe type). Также обратите внимание, что >>= должно быть написано только один раз в "тип бокс" (разные коробки, разные адаптации алгоритм.)

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

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

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

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

общий счастье!


ответ, конечно, "мы не". Как и во всех абстракциях, в этом нет необходимости.

Haskell не нуждается в абстракции монады. Это не обязательно для выполнения IO на чистом языке. The IO type заботится об этом сам по себе. Существующей монаде результате обессахаривания из do блоки могут быть заменены в результате обессахаривания bindIO, returnIO и failIO как определено в GHC.Base модуль. (Это не документированный модуль на hackage, поэтому я буду должны указать на источник для документации.) Так что нет, нет необходимости в абстракции монады.

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

в функциональных языках самый мощный инструмент для повторного использования кода состав функций. Старый добрый (.) :: (b -> c) -> (a -> b) -> (a -> c) оператор чрезвычайно мощный. Это позволяет легко писать крошечные функции и склеивать их вместе с минимальными синтаксическими или семантическими накладными расходами.

но бывают случаи, когда не совсем правильно. Что вы делаете, когда у вас есть foo :: (b -> Maybe c) и bar :: (a -> Maybe b)? foo . bar не typecheck, потому что b и Maybe b не одного типа.

но.. Это почти правильно. Ты просто хочешь немного свободы. Вы хотите быть способен лечить Maybe b как будто это были в основном b. Это плохая идея, чтобы просто относиться к ним как к одному и тому же типу. Это более или менее то же самое, что и нулевые указатели, которые Тони хор лихо назвал ошибка в миллиард долларов. Поэтому, если вы не можете рассматривать их как один и тот же тип, возможно, вы можете найти способ расширить механизм композиции (.) обеспечивает.

в этом случае важно действительно изучить теорию, лежащую в основе (.). К счастью, кто-то уже сделал это за нас. Оказывается, что комбинация (.) и id сформировать математическую конструкцию, известную как категория. Но есть и другие способы формирования категорий. Например, категория Kleisli позволяет немного дополнять составляемые объекты. Категория Kleisli для Maybe состоит из (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) и id :: a -> Maybe a. То есть объекты в категории дополняют (->) С Maybe, так что (a -> b) будет (a -> Maybe b).

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

  1. левый личность: id . f = f
  2. права личности: f . id = f
  3. ассоциативность: f . (g . h) = (f . g) . h

пока вы можете доказать, что ваш тип подчиняется этим трем законам, вы можете превратить его в категорию Kleisli. И что в этом такого? Оказывается, монады-это то же самое, что и категории Клейсли. Monad ' s return это то же самое, что Kleisli id. Monad ' s (>>=) не идентичны Kleisli (.), но оказывается, очень легко написать каждый с точки зрения другой. И законы жанра такие же, как законы монады, когда вы переводите их через разницу между (>>=) и (.).

так зачем вся эта суета? Зачем иметь Monad абстракция в языке? Как я уже упоминал выше, он позволяет повторно использовать код. Он даже позволяет повторно использовать код в двух разных измерениях.

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

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

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


: filter (> 3) [1..10], что составляет [4,5,6,7,8,9,10].

немного более сложная версия filter, который также передает аккумулятор слева направо, is

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

для всех i, таких, что i <= 10, sum [1..i] > 4, sum [1..i] < 25, мы можем написать

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

что составляет [3,4,5,6].

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

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4] равна [1,2,4,5,3,8,9]. Список передается как аккумулятор здесь. Код работает, потому что можно оставить монаду списка, поэтому все вычисления остаются чистыми (notElem не использовать >>= на самом деле, но может). Однако невозможно безопасно оставить монаду ввода-вывода (т. е. вы не можете выполнить действие ввода - вывода и вернуть чистое значение-значение всегда будет обернуто в монаду ввода-вывода). Другой пример-изменяемые массивы: после того, как вы оставили ST monad, где живет изменяемый массив, вы больше не можете обновлять массив в постоянное время. Поэтому нам нужна монадическая фильтрация из Control.Monad модуль:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterM выполняет монадическое действие для всех элементов из списка, дающего элементы, для которых монадическое действие возвращает True.

пример фильтрации с массивом:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

печать [1,2,4,5,3,8,9] как и ожидалось.

и версия с монадой IO, которая спрашивает, какие элементы вернуть:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Э. Г.

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

и в качестве последней иллюстрации,filterAccum можно определить в терминах filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

С StateT монада, которая используется под Худ - обычный тип данных.

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


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

наивно создание системы ввода-вывода для Haskell

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

main₀ :: String -> String
main₀ _ = "Hello World"

С lazyness, что простая подпись достаточно на самом деле создание интерактивных терминальных программ -очень общества, хотя. Самое неприятное, что мы можем выводить только текст. Что, если мы добавим еще несколько интересных возможностей?

data Output = TxtOutput String
            | Beep Frequency

main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

мило, но, конечно, гораздо более реалистичный "альтернативный выход" будет запись в файл. Но тогда тебе хотим читать от файлы. Есть шанс?

Ну, когда мы берем наши


монады просто удобная среда для решения класса проблем. Во-первых, монады должны быть функторы (т. е. должен поддерживать отображение, не глядя на элементы (или их тип)), они также должны принести обязательные (или цепочка) операция и способ создания монадического значения из типа элемента (return). Наконец,bind и return должно удовлетворять двум уравнениям (левое и правое тождества), также называемым законами монады. (В качестве альтернативы можно определить монады, чтобы иметь flattening operation вместо привязки.)

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

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

вот пример repl сеанс:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

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


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

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

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

теперь, одна интересная вещь о монадах, заключается в том, что результат композиции всегда имеет тип "M a", то есть значение внутри конверта, помеченного "M". Эта функция действительно хороша для реализации, например, четкого разделения между чистым и нечистым кодом: объявить все нечистые действия как функции типа "IO a" и не предоставлять никакой функции при определении монады ввода-вывода, чтобы вынуть значение "a" изнутри "IO a". В результате нет функция может быть чистой и в то же время вынимать значение из "IO a", потому что нет способа взять такое значение, оставаясь чистым (функция должна быть внутри монады "IO", чтобы использовать такое значение). (Примечание: ну, ничего не идеально, поэтому " IO смирительная рубашка "может быть сломана с помощью" unsafePerformIO : IO a -> a", тем самым загрязняя то, что должно было быть чистой функцией, но это должно использоваться очень экономно и когда вы действительно знаете, что не вводите какой-либо нечистый код с побочными эффектами.


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

Позвольте уточнить. У вас есть Int, String и Real и функции типа Int -> String, String -> Real и так далее. Вы можете легко комбинировать эти функции, заканчивая Int -> Real. Жизнь прекрасна.

затем, в один прекрасный день, вам нужно создать новая семья типа. Это может быть потому, что вам нужно рассмотреть возможность возврата значения no (Maybe), возврат ошибки (Either), множественные результаты (List) и так далее.

обратите внимание, что Maybe - это конструктор типа. Он принимает тип, как Int и возвращает новый тип Maybe Int. Первое, что нужно запомнить,ни один конструктор, ни монады.

конечно, вы хотите использовать конструктор типа в вашем коде, и вскоре вы закончите с такими функциями, как Int -> Maybe String и String -> Maybe Float. Теперь вы не можете легко комбинировать свои функции. Жизнь больше не хороша.

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