it-swarm.com.ru

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

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

343
cibercitizen1

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

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

    f(x) = 2 * x

    g(x,y) = x / y

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

    Решение: составьте функции. Если вы хотите сначала 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.

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

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

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

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

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

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

Всего счастья!

543
cibercitizen1

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

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

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

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

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

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

В этом случае важно действительно изучить теорию, лежащую в основе (.). К счастью, кто-то уже сделал это для нас. Оказывается, что комбинация (.) и id образует математическую конструкцию, известную как категория . Но есть и другие способы формирования категорий. Например, категория Клейсли позволяет немного дополнить составляемые объекты. Категория 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

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

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

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

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

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

210
Carl

Бенджамин Пирс сказал в TAPL

Систему типов можно рассматривать как вычисление своего рода статического приближения к поведению терминов в программе во время выполнения.

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

Как @Carl и sigfpe point, вы можете оборудовать тип данных всеми нужными вам операциями, не прибегая к монадам, классам типов или любым другим абстрактным вещам. Однако монады позволяют вам не только писать повторно используемый код, но и абстрагироваться от всех лишних деталей.

В качестве примера, скажем, мы хотим отфильтровать список. Самый простой способ - использовать функцию filter: filter (> 3) [1..10], которая равна [4,5,6,7,8,9,10].

Несколько более сложная версия filter, которая также передает аккумулятор слева направо,

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 на самом деле не использует >>=, но это возможно). Однако невозможно безопасно покинуть монаду IO (то есть вы не можете выполнить действие IO и ​​вернуть чистое значение - значение всегда будет заключено в IO Монада). Другой пример - изменяемые массивы: после того, как вы оставили монаду ST, в которой находится изменяемый массив, вы больше не можете обновлять массив в постоянное время. Итак, нам нужна монадическая фильтрация из модуля 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), но и одинаково обрабатывать пользовательские типы данных и встроенные примитивы.

24
user3237465

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

Наивное построение системы IO для Haskell

Самая простая мыслимая система IO для чисто функционального языка (и фактически того, с чего начинал Haskell) состоит в следующем:

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

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

data Output = TxtOutput String
            | Beep Frequency

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

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

Что ж, когда мы берем нашу программу main₁ и просто передаем файл в процесс (используя средства операционной системы), мы, по сути, реализуем чтение файла. Если бы мы могли запустить чтение файлов из языка Haskell ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Это будет использовать "интерактивную программу" String->[Output], передать ей строку, полученную из файла, и получить неинтерактивную программу, которая просто выполняет данную.

Здесь есть одна проблема: у нас нет понятия когда файл читается. Конечно, список [Output] дает хороший порядок output, но мы не получаем порядок, когда будет выполнено input.

Решение: сделать входные события также пунктами в списке дел.

data IO₀ = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Хорошо, теперь вы можете заметить дисбаланс: вы можете прочитать файл и сделать вывод зависимым от него, но вы не можете использовать содержимое файла, чтобы решить, например, также прочитайте другой файл. Очевидное решение: сделать результат событий ввода также чем-то типа IO, а не просто Output. Это, конечно, включает в себя простой вывод текста, но также позволяет читать дополнительные файлы и т.д ..

data IO₁ = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

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

  • main₃ выдает целый список действий. Почему бы нам просто не использовать подпись :: IO₁, которая имеет это как особый случай?

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

data IO₂ = TxtOut String IO₂
         | TxtIn (String -> IO₂)
         | Terminate

main₄ :: IO₂
main₄ = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Не так уж плохо!

Так какое отношение все это имеет к монадам?

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

getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂

Здесь, очевидно, есть образец, и нам лучше написать

type IO₃ a = (a -> IO₂) -> IO₂    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)

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

Во всяком случае, теперь мы достигли формулировки IO, которая имеет правильный экземпляр монады:

data IO₄ a = TxtOut String (IO₄ a)
           | TxtIn (String -> IO₄ a)
           | TerminateWith a

txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith

instance Functor IO₄ where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO₄ where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO₄ where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Очевидно, что это не эффективная реализация ввода-вывода, но в принципе это удобно.

18
leftaroundabout

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

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

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

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

3
mljrg

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

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

3
heisenbug

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

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

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

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

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

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

2
jdinunzio