Почему побочные эффекты моделируются как монады в Haskell?

Может ли кто-нибудь указать на то, почему нечистые вычисления в Haskell моделируются как монады?

Я имею в виду, что monad - это просто интерфейс с 4 операциями, так что же было причиной моделирования побочных эффектов?

Ответ 1

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

Так что для нечистой функции

f' :: Int -> Int

мы добавляем RealWorld к рассмотрению

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

тогда f снова чисто. Мы определяем параметризованный тип данных IO a = RealWorld → (a, RealWorld), поэтому нам не нужно много раз вводить RealWorld

f :: Int -> IO Int

Для программиста непосредственная обработка RealWorld слишком опасна - в частности, если программист получает в свои руки значение типа RealWorld, он может попытаться скопировать его, что в принципе невозможно. (Подумайте, например, о попытке скопировать всю файловую систему. Где бы вы ее поместили?) Поэтому наше определение IO также включает в себя состояния всего мира.

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

getLine :: IO String               = RealWorld -> (String, RealWorld)
getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)

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

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Здесь мы видим шаблон: функции вызываются так:

...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Таким образом, мы могли бы определить оператор ~~~ чтобы связать их:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b, RealWorld))
      -> (b -> RealWorld -> (c, RealWorld))
      ->       RealWorld -> (c, RealWorld)
(f ~~~ g) worldX = let (resF, worldY) = f worldX in
                        g resF worldY

тогда мы могли бы просто написать

printFile = getLine ~~~ getContents ~~~ putStrLn

не касаясь реального мира.


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

upperCase :: String -> String

Но чтобы попасть в реальный мир, он должен вернуть IO String. Легко поднять такую функцию:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

это можно обобщить:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

так что impureUpperCase = impurify. upperCase impureUpperCase = impurify. upperCase, и мы можем написать

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Примечание: обычно мы пишем getLine ~~~ getContents ~~~ (putStrLn. upperCase))


Теперь давайте посмотрим, что мы сделали:

  1. Мы определили оператор (~~~) :: IO b → (b → IO c) → IO c который связывает две нечистые функции вместе
  2. Мы определили функцию impurify :: a → IO a которая преобразует чистое значение в нечистое.

Теперь мы делаем идентификацию (>>=) = (~~~) и return = impurify, и видите? У нас есть монада.


(Чтобы проверить, действительно ли это монада, нужно выполнить несколько аксиом:

(1) return a >>= f = fa

  impurify a               = (\world -> (a, world))
 (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX 
                             in f resF worldY
                           = let (resF, worldY) =            (a, worldX))       
                             in f resF worldY
                           = f a worldX

(2) f >>= return = f

  (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX 
                              in f resF worldY
                            = let (resF, worldY) = (a, worldX)     
                              in f resF worldY
                            = f a worldX

(3) f >>= (\x → gx >>= h) = (f >>= g) >>= h

Упражнение.)

Ответ 2

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

Этот вопрос содержит широко распространенное недоразумение. Примесь и Монада являются независимыми понятиями. Примесь не моделируется Монадой. Скорее, есть несколько типов данных, таких как IO, которые представляют собой обязательные вычисления. А для некоторых из этих типов крошечная доля их интерфейса соответствует шаблону интерфейса, называемому "Monad". Более того, не существует известного чисто/функционального/дентивативного объяснения IO (и вряд ли оно будет, если учесть цель IO "sin bin"), хотя существует широко рассказанная история о World → (a, World) быть смыслом IO a. Эта история не может правдиво описать IO, потому что IO поддерживает параллелизм и недетерминизм. История даже не работает, когда для детерминированных вычислений, которые позволяют взаимодействию в середине вычислений с миром.

Для более подробного объяснения см. Этот ответ.

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

Ответ 3

Как я понимаю, кто-то, называемый Eugenio Moggi, сначала заметил, что ранее неясную математическую конструкцию, называемую "монадой", можно было бы использовать для моделирования стороны эффекты на компьютерных языках и, следовательно, определять их семантику с использованием исчисления лямбда. Когда разрабатывался Haskell, были различные способы, в которых моделировались нечистые вычисления (подробнее см. Статью "рубашка для волос" Саймона Пейтона Джонса) но когда Фил Вадлер представил монады, быстро стало очевидно, что это был "Ответ". А остальное - история.

Ответ 4

Может ли кто-нибудь указать некоторые причины, по которым неудобные вычисления в Haskell моделируются как монады?

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

Это означает, что вам нужно будет найти какой-то тип IO a, который моделирует нечеткое вычисление. Затем вам нужно знать, как сочетать эти вычисления, которые применяются последовательно (>>=), и поднимать значение (return) являются наиболее очевидными и основными.

С этими двумя вы уже определили монаду (даже не думая об этом);)

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

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

Ответ 5

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

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

Итак, что мы можем сказать об этой операции? Он ассоциативный; то есть, если мы объединим три побочных эффекта, неважно, какой порядок мы выполняем. Если мы делаем (записываем файл, затем читаем сокет), то выключение компьютера, это то же самое, что и делать файл записи (затем читать сокет, затем выключать компьютер). Но это не коммутативно: ( "написать файл", затем "удалить файл" ) - это другой побочный эффект ( "удалить файл" , затем "записать файл" ). И у нас есть личность: специальный побочный эффект "никаких побочных эффектов" работает ( "никаких побочных эффектов", а затем "удалить файл" - это тот же побочный эффект, что и "удалить файл" ). В этот момент любой математик думает "Группа!". Но группы имеют инверсии, и нет никакого способа инвертировать побочный эффект в целом; "удалить файл" необратим. Таким образом, у нас осталась структура моноида, что означает, что наши побочные функции должны быть монадами.

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

Ответ 6

Это на самом деле довольно чистый способ думать об I/O функциональным способом.

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

Монады - просто симпатичный синтаксис именно для этого.

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

Ответ 7

AFAIK, причина состоит в том, чтобы иметь возможность включать проверки побочных эффектов в систему типов. Если вы хотите узнать больше, слушайте те SE-Radio эпизоды: Эпизод 108: Саймон Пейтон Джонс по функциональному программированию и Haskell Эпизод 72: Эрик Мейер на LINQ

Ответ 8

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

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

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

Здесь приходит монада. На самом деле, я не уверен, что не существует другой конструкции с подобными необходимыми свойствами, но дело в том, что монада обладает этими свойствами, поэтому ее можно использовать (и она успешно используется). Главное свойство состоит в том, что мы не можем убежать от него. Интерфейс Monad не имеет операций, чтобы избавиться от монады вокруг нашего значения. Другие (не IO) монады предоставляют такие операции и позволяют сопоставлять шаблоны (например, возможно), но эти операции не находятся в интерфейсе monad. Другим требуемым свойством является способность выполнять операции цепочки.

Если мы думаем о том, что нам нужно в терминах системы типов, мы приходим к тому, что нам нужен тип с конструктором, который может быть обернут вокруг любой доли. Конструктор должен быть закрытым, поскольку мы запрещаем избегать его (например, сопоставления с образцом). Но нам нужна функция, чтобы поместить значение в этот конструктор (здесь приходит на ум). И нам нужен способ цепных операций. Если мы подумаем об этом в течение некоторого времени, мы придем к тому, что операция цепочки должна иметь тип как → = has. Итак, мы приходим к чему-то очень похожему на монаду. Я думаю, если мы сейчас проанализируем возможные противоречивые ситуации с этой конструкцией, мы придем к аксиомам монады.

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

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

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