catpad (catpad) wrote,
catpad
catpad

Category:

Yet Another Another Another Monad Tutorial

Итак, монады. Я решил поделиться с читателями своим просветлением. Далее следует самое краткое объяснение монад, которое когда-либо существовало. Оно не совсем программистское, а больше философское, но по-моему, именно так и нужно понимать монады.


В принципе, объяснение монад сводится к одному предложению: монады создают новые миры.
Представим, что среда, в которой выполняется некоторая функция Хаскела - это такой закрытый мир. Например,

double :: Int -> Int
double x = x*2

У этой функции нет никаких побочных эффектов: для каждого данного х, функия всегда будет вычислять один и тот же результат, и текущий мир будет абсолютно детерминированным.
Что делать, если нужно прочитать входные данные с терминала ? Функция getLine не может существовать в мире Хаскела, потому что каждый раз она будет производить какое-то другое действие.
Для этого придумали выход: так как мир, в котором выполняется любая функция Хаскела обязан быть совершенным, то есть всегда предсказуемым и без побочных эффектов, мы создадим новый мир, в котором будет существовать функция getLine после её выполнения.
функция getLine имеет следующий тип:

getLine :: IO String

то есть, она ничего не получает на входе, а значением её является IO String - так называемое монадическое значение, а я буду называть его миром типа IO String. Это означает, что вместо обычной строки типа String, функция возвращает новый мир, в котором неким магическим образом существует эта самая строка. Откуда она взялась, как попала в этот новый мир монады IO - нам неизвестно. То есть, нам-то конечно известно (она была прочитана с терминала), но смысл как раз и заключается в том, что обитателям этого нового мира под названием IO String это неведомо, и узнать об этом ни при каких обстоятельствах они не могут. Для них (то есть для Хаскела), выполнение программы просто продолжается в новом, совершенном мире без каких-либо побочных эффектов.

Примечание: мне кажется, что самая большая ошибка учебников по монадам состоит в том, что они называют IO String либо container, либо action. Оба определения совершенно запутывают читателя.
IO String - это далеко не просто контейнер, который содержит какую-то переменную. Из контейнера всегда можно извлечь содержащееся в нём значение. IO String как результат функции - это именно целый новый мир, который создаётся Хаскелом именно для того, чтобы ничего не знать о предыдущей жизни функции, произведшей действие. Поэтому и извлекать из него ничего не надо (да и не получится). Называть же этот мир просто "действием" - это вообще позор какой-то. Смысл-то как раз и заключается в том, что само действие происходит ещё до начала времён, то есть до создания мира IO String.

То же самое, только наоборот, происходит с функцией

putStrLn :: String -> IO ()

Эта функия существует в некоем мире W1, в котором на входе ей дают строку типа String. Функция эта, не производя никаких побочных эффектов в мире W1, возвращает новый мир W2 типа IO (), в котором опять же не бывает никаких побочных эффектов. Во время создания этого нового мира W2, втайне от обитателей обоих миров, функция putStrLn вывела на терминал полученную строку, но никто об этом, кроме внешних наблюдателей (нас) не знает. Мир W2 остаётся непорочным.

В самом общем виде монадическая функция мира W1

f :: a -> m b

принимает переменную типа а и возвращает новый мир W2 типа m b, в котором существует переменная типа b. Возможно, что при создании этого мира, функция f выполнила некую тайную работу, о которой ни в мире W1, ни в мире W2 никто ничего не знает.

Хитрость монад заключается в том, что извлечь "чистое" значение типа b из мира m b невозможно - просто потому что никакого "чистого" значения типа b не существует вне мира W2, созданного монадой m b. Просто Хаскел перестал существовать в мире W1 и продолжил своё существование в мире W2. Вопрос об "извлечении" чего-либо из текущего мира Хаскела лишён всякого смысла.

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

(>>=) :: m a -> (a -> m b) -> m b

Он принимает монадический мир m a (в котором существует переменная типа а), функцию над этим типом (a -> m b) и возвращает новый монадический мир m b, в котором существует переменная типа b. Другими словами, оператор >>= позволяет совершить некоторую операцию на переменной типа а, существующей в своём монадическом мире, и как результат создаёт новый монадический мир, в котором существует переменная типа b.
Самый простой пример - функция echo, которая читает строку с терминала и печатает её:

echo = getLine >>= putStrLn

Если перевести это на язык миров, то получается вот что:

1) Программа выполняется в мире W1.
2) Функция getLine :: IO String создаёт новый мир W2 типа IO String, в котором с самого момента его создания существует строка, прочитанная с терминала ещё до сотворения мира. Извлечь её оттуда никак нельзя, но можно
3) применить к ней функцию putStrLn :: String -> IO (), которая по возвращении создаст новый мир W3, в котором функция putStrLn не сделала ровным счётом ничего (она даже и значения-то никакого не возращает). Однако, мы знаем, что ещё до создания мира W3 putStrLn напечатала на терминале строку, которую она получила в качестве аргумента в мире W2.
4) Программа продолжает выполняться в мире W3 как ни в чём не бывало.

Вот, собственно, и всё, что нужно знать для понимания монад. Остальные детали можно узнать из многочисленных tutorials.

Tags: программирование
Subscribe

  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 5 comments