catpad: (Default)
catpad ([personal profile] catpad) wrote2011-06-03 05:57 pm

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.

[identity profile] potan.livejournal.com 2011-06-03 09:09 am (UTC)(link)
Это объяснение монады IO, а не монад вообще.
Создание миров хорошо иллюстрируется концепцией "уникальных типов" (языки Mercury и Clean). Если скрестить это с монадой State, то получится монада IO. Но монада State сама по себе заслуживает того, что бы с ней разобраться.

[identity profile] catpad.livejournal.com 2011-06-04 08:10 am (UTC)(link)
Совершенно не согласен. Это объяснение монад на примере IO. У всех монад один смысл, и этот-то смысл я и попытался здесь передать.
У них разное назначение, но смысл один.

[identity profile] potan.livejournal.com 2011-06-05 03:42 pm (UTC)(link)
Что-то я не улавливаю связь ни с Maybe, ни с [].

[identity profile] catpad.livejournal.com 2011-06-06 02:46 am (UTC)(link)
Вот цитата отсюда (http://mvanier.livejournal.com/3917.html). В каких случаях применяются монады:

1.Functions that may do (terminal or file) input/output. This corresponds to the IO monad, so we could write this as

f :: a --[IO]--> b

2.Functions that may raise exceptions. This correspond to various kinds of error monads:

f :: a --[error]--> b

3. Functions that can interact with global or local state. This corresponds to the State monad:

f :: a --[State s]--> b

4. Functions that can fail. This corresponds to the Maybe monad:

f :: a --[Maybe]--> b

5. Functions that can return multiple values in parallel. This corresponds to the list monad:

f :: a --[list]--> b

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


[identity profile] taganay.livejournal.com 2011-06-03 06:01 pm (UTC)(link)
Круто. Похоже, что надо будет покурить Хаскел - интересный подход.